-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatom.xml
More file actions
441 lines (234 loc) · 313 KB
/
atom.xml
File metadata and controls
441 lines (234 loc) · 313 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Alan Lee</title>
<subtitle>Always Learning</subtitle>
<link href="https://alanlee.fun/atom.xml" rel="self"/>
<link href="https://alanlee.fun/"/>
<updated>2026-04-14T02:51:14.909Z</updated>
<id>https://alanlee.fun/</id>
<author>
<name>Alan Lee</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>chatgpt 还是太啰嗦</title>
<link href="https://alanlee.fun/2026/03/11/verbose-chatgpt/"/>
<id>https://alanlee.fun/2026/03/11/verbose-chatgpt/</id>
<published>2026-03-11T11:03:00.000Z</published>
<updated>2026-04-14T02:51:14.909Z</updated>
<content type="html"><![CDATA[<!-- excerpt --></p><!-- toc --><h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>最近要上一个用到 clickhouse 的服务,我没有这方面经验,之前仅仅在部署 langfuse 时遇到过,但是也只是跟着 chatgpt 设置了一些表的 ttl。所以这次还是高度依赖 chatgpt 帮我设计表、写初步代码、部署和进行其他优化讨论。</p><p>刚开始进行挺顺利,帮我简单入门了下 clickhouse,经过一系列讨论,写了 sql 和插入查询代码。终于该部署了。我在本地测试时这一步也很顺利,然后在发布到线上后,容器死活起不来,一直在 restarting,通过日志可以看到如下错误(我手动进行了 wrap 以更方便查看):</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">{} <Error> Application: Caught exception while loading metadata:</span><br><span class="line">Code: 722. DB::Exception: Waited job failed: Code: 695.</span><br><span class="line">DB::Exception: Load job 'load table system.latency_log' failed:</span><br><span class="line">Code: 74. DB::ErrnoException: Cannot read from file 28: ,</span><br><span class="line">errno: 1, strerror: Operation not permitted: Cannot attach table</span><br><span class="line">system.latency_log from metadata file ...</span><br></pre></td></tr></table></figure><p>后面还有很长我就不贴了,看起来像是 sql 语句。</p><h2 id="啰嗦的-chatgpt"><a href="#啰嗦的-chatgpt" class="headerlink" title="啰嗦的 chatgpt"></a>啰嗦的 chatgpt</h2><p>没经验的我第一眼看起来像是权限问题,可能是挂载的目录权限有问题,问了 chatgpt 5.4 thinking,也说是这个问题:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://files.seeusercontent.com/2026/03/11/8Iwz/image.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://files.seeusercontent.com/2026/03/11/8Iwz/image.png" alt=""></a></div><p>后面给我的详细原因也是这个:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://files.seeusercontent.com/2026/03/11/bw6L/image-1.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://files.seeusercontent.com/2026/03/11/bw6L/image-1.png" alt=""></a></div><p>解决方案也是删除挂载目录,重新来:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://files.seeusercontent.com/2026/03/11/J2zj/image-2.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://files.seeusercontent.com/2026/03/11/J2zj/image-2.png" alt=""></a></div><p>我现在本地复现,复现不了。然后我尝试按照它提供的方向进行调试。我当前的持久化方式是显式挂载到本地目录,类似这样:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">volumes:</span><br><span class="line"> <span class="operator">-</span> .<span class="operator">/</span>data<span class="operator">/</span>clickhouse:<span class="operator">/</span>var<span class="operator">/</span>lib<span class="operator">/</span>clickhouse</span><br></pre></td></tr></table></figure><p>于是我进行了如下尝试:</p><ol><li><p>挂载到 docker 管理的 volume:</p> <figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">services:</span><br><span class="line">...</span><br><span class="line">volumes:</span><br><span class="line"> <span class="operator">-</span> clickhouse_data:<span class="operator">/</span>var<span class="operator">/</span>lib<span class="operator">/</span>clickhouse</span><br><span class="line">...</span><br><span class="line"></span><br><span class="line">volumes:</span><br><span class="line"> clickhouse_data:</span><br></pre></td></tr></table></figure><p> 这样数据完全由 docker 管理,而且也是全新的 volume,应该不会再存在权限问题。</p></li><li><p>删除该目录。</p></li><li>不再挂载该目录。</li></ol><p>以上均无效,仍然报之前的错误。由于我无法登录线上机器,所以哪怕查看 docker 日志这么小的操作也是比较麻烦。再加上其中各种问题,其中还需要找运维帮忙,以及过程中和 chatgpt 的多次交流,昨天一下午都在搞这个事情。</p><p>当我尝试完 1 无效后,去把结果丢给 chatgpt,它认为可能是我指定的那个 volume 是一个旧的 volume,里面有残存数据;或者 clickhouse 镜像版本太高;或者镜像的 seccomp 触发了容器内文件异步读取的 <code>Operation not permitted</code> ,由此它推荐我进行如下操作:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://files.seeusercontent.com/2026/03/11/xw2R/image-3.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://files.seeusercontent.com/2026/03/11/xw2R/image-3.png" alt=""></a></div><p>从事后诸葛亮来看,其实它在这里已经提到了正确的解决方案:就是这里的 3。只是到这里,我的耐心已经被消耗大半,它的回复每次又非常啰嗦(尽管我已经在个性化中对此进行了限制,比之前有好转,但是仍然不够精炼),导致我没有再进行测试的耐心了。而且由于我无法登录线上机器,所以 1 我实现不了,要实现就得找运维,又得来回好一会儿。对于 2,那也很麻烦,降级就意味着要下载新镜像,还要再推送到我们自己的仓库中,改 compose 文件,鉴于我们的网速和这个流程,我也就放弃了。而且我也不赞成 1 和 2 是潜在的原因。这就导致正确方案被我忽略了,我已经没有耐心听它的意见了。</p><p>接下来更是偏离了方向。由于我在配置中禁用了很多系统表,所以我问它是不是这些配置导致的。它回答有可能:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://files.seeusercontent.com/2026/03/11/s5Oq/image-4.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://files.seeusercontent.com/2026/03/11/s5Oq/image-4.png" alt=""></a></div><p>然后建议我先不要挂载配置。我接着问它有无可能是我的挂载方式覆盖了原本的配置导致的:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://files.seeusercontent.com/2026/03/11/dL5z/image-5.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://files.seeusercontent.com/2026/03/11/dL5z/image-5.png" alt=""></a></div><p>我对这个完全不懂,所以才会怀疑是不是覆盖了配置文件。然后我又对这个挂载方式进行了一些调试,无果。事后证明这个目录就是用来存放额外的配置的,默认是空的,所以不存在我怀疑的这个问题。理想情况是此时 chatgpt 应该要指出来这点,直接告诉我这个不是原因。</p><p>现在来看上面这些,就让我想到了 llm 的“大海捞针” The Needle In a Haystack 测试,这就像 llm 反过来测试我的大海捞针能力一样,测试我能不能在一堆啰嗦的废话中找到那个 needle。关于废话这点,5.3 instant <a href="https://openai.com/zh-Hans-CN/index/gpt-5-3-instant/">说是有所改进</a>,当初我的测试也是似乎比之前好点,不过这个改进是不是没用到 5.4 上啊:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://files.seeusercontent.com/2026/03/11/9kwO/00c99fc7615a77b68bd479a076d44d66.png" title="5.3 instant 的改进" data-caption="5.3 instant 的改进" data-fancybox="default"><img class="fig-img" src="https://files.seeusercontent.com/2026/03/11/9kwO/00c99fc7615a77b68bd479a076d44d66.png" alt="5.3 instant 的改进"></a><span class="caption">5.3 instant 的改进</span></div><p>然后这个问题我就先搁置了,交给运维,我先下班了,被这玩意儿折腾得不行。哦对了,我后来还想到这个大海捞针的问题,担心 context 可能太大了,模型性能下降了。所以我又开了一个新会话,把报错、当前的 compose 文件、当前的 clickhouse 配置文件给它:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://files.seeusercontent.com/2026/03/11/9Iqw/image-6.png" title="回答的一部分" data-caption="回答的一部分" data-fancybox="default"><img class="fig-img" src="https://files.seeusercontent.com/2026/03/11/9Iqw/image-6.png" alt="回答的一部分"></a><span class="caption">回答的一部分</span></div><p>相对来说要比之前的好点,但是之前那个啰嗦的问题仍在。它的这种表达方式和我当时的心情,成功让我错过了正确方案。</p><h2 id="精准的-claude"><a href="#精准的-claude" class="headerlink" title="精准的 claude"></a>精准的 claude</h2><p>第二天上班后,我想着用 claude 试一试,虽然我没有 opus 可用,sonnet 试试也可以。于是我把相同的内容发给 sonnet 4.6,结果一针见血,直击要害:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://files.seeusercontent.com/2026/03/11/Qwv5/image-7.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://files.seeusercontent.com/2026/03/11/Qwv5/image-7.png" alt=""></a></div><p>修复 2 无效,1 是正确方案。简明扼要,直击要害。sonnet 4.6 完美诠释了这点。要不是 anthropic 封号和 ip 封的厉害,我也很想开他们家会员。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>不过回过头来,chatgpt 大部分时间还是不错的。虽然看起来我是在表扬 sonnet 批评 chatgpt,但是我最后想说的是,当你被一个 bot 气得不行的时候,不要着急,歇一歇,换另一个 bot 试试,众人拾柴火焰高,在座的各位都发表发表意见,头脑风暴一下,说不定解决方案就来了。</p><p>但总之,说话不要太啰嗦!</p><h2 id="END"><a href="#END" class="headerlink" title="END"></a>END</h2>]]></content>
<summary type="html"><p>本文对比了我在解决 ClickHouse 部署问题时使用 ChatGPT 和 Claude 的体验。ChatGPT 的回复过于啰嗦,错过了关键的解决方案,导致了挫败感。而 Claude 提供了简洁明了的解答,帮助我迅速解决了问题。[ChatGPT 5.4 Thinking]<br></summary>
<category term="LLM" scheme="https://alanlee.fun/tags/LLM/"/>
<category term="ChatGPT" scheme="https://alanlee.fun/tags/ChatGPT/"/>
<category term="Claude" scheme="https://alanlee.fun/tags/Claude/"/>
</entry>
<entry>
<title>MCP 简易指南(二):概念</title>
<link href="https://alanlee.fun/2025/06/14/introducing-mcp-part-2/"/>
<id>https://alanlee.fun/2025/06/14/introducing-mcp-part-2/</id>
<published>2025-06-14T12:46:00.000Z</published>
<updated>2026-04-14T02:51:14.902Z</updated>
<content type="html"><![CDATA[<!-- excerpt --></p><!-- toc --><p><a href="https://alanlee.fun/2025/06/03/introducing-mcp-part-1/">上一篇</a>重点讲了什么是 mcp、为何使用 mcp 以及如何使用。本来想把概念一起都写到那里,但是觉得太长了,干脆就把概念介绍放到了这里。</p><h1 id="核心概念-plus"><a href="#核心概念-plus" class="headerlink" title="核心概念 plus"></a>核心概念 plus</h1><h2 id="Transport-类型"><a href="#Transport-类型" class="headerlink" title="Transport 类型"></a>Transport 类型</h2><p>刚才我们在 demo 里用的是 <code>transport="streamable-http"</code> ,实际上 transport 有 3 种类型:</p><ul><li>stdio(默认)</li><li>sse</li><li>streamable-http</li></ul><p>第一种是命令行启动 server 程序(不是 http),比如 <code>uv --directory /ABSOLUTE/PATH/TO/PARENT/FOLDER/get_current_time run get_current_time.py</code> ,实际上就是直接运行 python 程序。你填入 inspector 或者 postman 的时候就是填入启动命令。</p><p>后两种都是 http 形式的,最终会暴露出来一个 url 来提供调用,sse 默认的端点是 <code>/sse</code> ,streamable-http 默认是 <code>/mcp</code> ,而且根据<a href="https://github.com/modelcontextprotocol/python-sdk/tree/main?tab=readme-ov-file#streamable-http-transport">文档</a>,后者已经在逐渐替换前者了,所以建议使用后者进行开发。两者区别如下:</p><div class="table-container"><table><thead><tr><th></th><th>SSE</th><th>streamable-http</th></tr></thead><tbody><tr><td><strong>endpoint</strong></td><td><code>/sse</code></td><td><code>/mcp</code></td></tr><tr><td><strong>启动参数</strong></td><td><code>transport="sse"</code></td><td><code>transport="streamable-http"</code></td></tr><tr><td><strong>推荐场景</strong></td><td>兼容老客户端、本地开发</td><td>生产环境、大并发、云部署</td></tr><tr><td><strong>是否支持流式</strong></td><td>支持(单向)</td><td>支持(更灵活,多格式)</td></tr><tr><td><strong>多节点/云原生</strong></td><td>不友好</td><td>非常友好</td></tr><tr><td><strong>负载均衡</strong></td><td>难</td><td>易</td></tr><tr><td><strong>Session管理</strong></td><td>简单/无状态</td><td>支持 stateful/stateless/resume</td></tr></tbody></table></div><h2 id="Tools"><a href="#Tools" class="headerlink" title="Tools"></a>Tools</h2><p>如果用过 openai 的 function calling,那么应该对这个很熟了,本质上就是将函数定义以一定格式传给模型。</p><p>工具由如下结构定义:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> name<span class="punctuation">:</span> string; <span class="comment">// 工具的唯一标识符</span></span><br><span class="line"> description?<span class="punctuation">:</span> string; <span class="comment">// 易读的工具描述</span></span><br><span class="line"> inputSchema<span class="punctuation">:</span> <span class="punctuation">{</span> <span class="comment">// 工具参数的 JSON schema</span></span><br><span class="line"> type<span class="punctuation">:</span> <span class="string">"object"</span><span class="punctuation">,</span></span><br><span class="line"> properties<span class="punctuation">:</span> <span class="punctuation">{</span> ... <span class="punctuation">}</span> <span class="comment">// 工具参数</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> annotations?<span class="punctuation">:</span> <span class="punctuation">{</span> <span class="comment">// 可选,关于工具行为的注释</span></span><br><span class="line"> title?<span class="punctuation">:</span> string; <span class="comment">// 易读的工具标题</span></span><br><span class="line"> readOnlyHint?<span class="punctuation">:</span> boolean; <span class="comment">// 如果 true,那么工具不会改变其环境(只读)。</span></span><br><span class="line"> destructiveHint?<span class="punctuation">:</span> boolean; <span class="comment">// 如果 true,那么工具可能会执行破坏性的更新。</span></span><br><span class="line"> idempotentHint?<span class="punctuation">:</span> boolean; <span class="comment">// 如果 true,那么对该工具使用同样参数重复调用,不会有额外影响。</span></span><br><span class="line"> openWorldHint?<span class="punctuation">:</span> boolean; <span class="comment">// 如果 true,那么工具可能会与外部实体交互。</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><p>这么说可能有点抽象,我们来看下刚才的 <code>get_current_time</code> 工具的定义:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"tools"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"get_current_time"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"description"</span><span class="punctuation">:</span> <span class="string">"\n Returns the current date and time in the 'Asia/Shanghai' timezone.\n\n Returns:\n datetime: The current datetime object localized to 'Asia/Shanghai' timezone.\n "</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"inputSchema"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"type"</span><span class="punctuation">:</span> <span class="string">"object"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"properties"</span><span class="punctuation">:</span> <span class="punctuation">{</span><span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"title"</span><span class="punctuation">:</span> <span class="string">"get_current_timeArguments"</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><p>其他还有执行系统命令和调用 api 的工具等例子,可以参考<a href="https://modelcontextprotocol.io/docs/concepts/tools#example-tool-patterns">这里</a>。</p><p>我们在使用调试工具连接 mcp server 后,会自动发起 ListToolsRequest 请求来获取可用的工具:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2025/06/14/dD4NucqonPIUl38.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2025/06/14/dD4NucqonPIUl38.png" alt=""></a></div><h2 id="Resources"><a href="#Resources" class="headerlink" title="Resources"></a>Resources</h2><p>资源就是 server 希望提供给 client 的数据,包括文本数据和二进制数据:</p><ul><li>文本数据:utf-8 编码的文本数据,例如代码、日志、JSON 和纯文本等。</li><li>二进制数据:PDF、图像、音频和视频等。</li></ul><p>每个资源都有一个唯一标识 URI,格式为 <code>[protocol]://[host]/[path]</code>,比如我们常见的 pg 数据库可以标识为 <code>postgres://database/customers/schema</code> ,文件可以标识为 <code>file:///home/user/documents/report.pdf</code> 。另外 server 也可以定义自己的 URI 格式。</p><p>server 在返回资源列表时,每个资源一般使用如下格式返回:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> uri<span class="punctuation">:</span> string; <span class="comment">// 资源 URI</span></span><br><span class="line"> name<span class="punctuation">:</span> string; <span class="comment">// 人类可读的名字</span></span><br><span class="line"> description?<span class="punctuation">:</span> string; <span class="comment">// 可选,资源描述,这个会提供给 llm,使其更好的理解该资源。</span></span><br><span class="line"> mimeType?<span class="punctuation">:</span> string; <span class="comment">// 可选,MIME type,如v text/plain</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><p>如果一个资源的 URI 是动态的,那么上述 <code>uri</code> 字段可以换成 <code>uriTemplate</code> ,其格式遵循 RFC 6570。</p><p>client 可以通过 <code>resources/read</code> 来读取可用的资源,server 返回资源列表。另外在资源更新时 server 也可以通知 client,具体可以见 <a href="https://modelcontextprotocol.io/docs/concepts/resources#resource-updates">Resource updates</a>。</p><p>一个实现例子:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">app = Server(<span class="string">"example-server"</span>)</span><br><span class="line"></span><br><span class="line"><span class="meta">@app.list_resources()</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">list_resources</span>() -> <span class="built_in">list</span>[types.Resource]:</span><br><span class="line"> <span class="keyword">return</span> [</span><br><span class="line"> types.Resource(</span><br><span class="line"> uri=<span class="string">"file:///logs/app.log"</span>,</span><br><span class="line"> name=<span class="string">"Application Logs"</span>,</span><br><span class="line"> mimeType=<span class="string">"text/plain"</span></span><br><span class="line"> )</span><br><span class="line"> ]</span><br><span class="line"></span><br><span class="line"><span class="meta">@app.read_resource()</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">read_resource</span>(<span class="params">uri: AnyUrl</span>) -> <span class="built_in">str</span>:</span><br><span class="line"> <span class="keyword">if</span> <span class="built_in">str</span>(uri) == <span class="string">"file:///logs/app.log"</span>:</span><br><span class="line"> log_contents = <span class="keyword">await</span> read_log_file()</span><br><span class="line"> <span class="keyword">return</span> log_contents</span><br><span class="line"></span><br><span class="line"> <span class="keyword">raise</span> ValueError(<span class="string">"Resource not found"</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># Start server</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">with</span> stdio_server() <span class="keyword">as</span> streams:</span><br><span class="line"> <span class="keyword">await</span> app.run(</span><br><span class="line"> streams[<span class="number">0</span>],</span><br><span class="line"> streams[<span class="number">1</span>],</span><br><span class="line"> app.create_initialization_options()</span><br><span class="line"> )</span><br></pre></td></tr></table></figure><h2 id="Prompts"><a href="#Prompts" class="headerlink" title="Prompts"></a>Prompts</h2><p>在 mcp 中,prompts 其实指的是 prompt template,即带有参数的 prompt,说白了就是 python 中的 f-string。</p><p>这个 template 的定义如下:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> name<span class="punctuation">:</span> string; <span class="comment">// prompt 的唯一标识符</span></span><br><span class="line"> description?<span class="punctuation">:</span> string; <span class="comment">// 人类可读的描述</span></span><br><span class="line"> arguments?<span class="punctuation">:</span> <span class="punctuation">[</span> <span class="comment">// 可选,参数列表</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> name<span class="punctuation">:</span> string; <span class="comment">// 参数标识符</span></span><br><span class="line"> description?<span class="punctuation">:</span> string; <span class="comment">// 参数描述</span></span><br><span class="line"> required?<span class="punctuation">:</span> boolean; <span class="comment">// 该参数是否必需</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><p>和之前的资源一样,client 也可以通过请求来获取可用的 prompt 列表,以及 server 可以通知 client prompt 有更新。</p><p>一个实现例子:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> mcp.server <span class="keyword">import</span> Server</span><br><span class="line"><span class="keyword">import</span> mcp.types <span class="keyword">as</span> types</span><br><span class="line"></span><br><span class="line"><span class="comment"># Define available prompts</span></span><br><span class="line">PROMPTS = {</span><br><span class="line"> <span class="string">"git-commit"</span>: types.Prompt(</span><br><span class="line"> name=<span class="string">"git-commit"</span>,</span><br><span class="line"> description=<span class="string">"Generate a Git commit message"</span>,</span><br><span class="line"> arguments=[</span><br><span class="line"> types.PromptArgument(</span><br><span class="line"> name=<span class="string">"changes"</span>,</span><br><span class="line"> description=<span class="string">"Git diff or description of changes"</span>,</span><br><span class="line"> required=<span class="literal">True</span></span><br><span class="line"> )</span><br><span class="line"> ],</span><br><span class="line"> ),</span><br><span class="line"> <span class="string">"explain-code"</span>: types.Prompt(</span><br><span class="line"> name=<span class="string">"explain-code"</span>,</span><br><span class="line"> description=<span class="string">"Explain how code works"</span>,</span><br><span class="line"> arguments=[</span><br><span class="line"> types.PromptArgument(</span><br><span class="line"> name=<span class="string">"code"</span>,</span><br><span class="line"> description=<span class="string">"Code to explain"</span>,</span><br><span class="line"> required=<span class="literal">True</span></span><br><span class="line"> ),</span><br><span class="line"> types.PromptArgument(</span><br><span class="line"> name=<span class="string">"language"</span>,</span><br><span class="line"> description=<span class="string">"Programming language"</span>,</span><br><span class="line"> required=<span class="literal">False</span></span><br><span class="line"> )</span><br><span class="line"> ],</span><br><span class="line"> )</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment"># Initialize server</span></span><br><span class="line">app = Server(<span class="string">"example-prompts-server"</span>)</span><br><span class="line"></span><br><span class="line"><span class="meta">@app.list_prompts()</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">list_prompts</span>() -> <span class="built_in">list</span>[types.Prompt]:</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">list</span>(PROMPTS.values())</span><br><span class="line"></span><br><span class="line"><span class="meta">@app.get_prompt()</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">get_prompt</span>(<span class="params"></span></span><br><span class="line"><span class="params"> name: <span class="built_in">str</span>, arguments: <span class="built_in">dict</span>[<span class="built_in">str</span>, <span class="built_in">str</span>] | <span class="literal">None</span> = <span class="literal">None</span></span></span><br><span class="line"><span class="params"></span>) -> types.GetPromptResult:</span><br><span class="line"> <span class="keyword">if</span> name <span class="keyword">not</span> <span class="keyword">in</span> PROMPTS:</span><br><span class="line"> <span class="keyword">raise</span> ValueError(<span class="string">f"Prompt not found: <span class="subst">{name}</span>"</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> name == <span class="string">"git-commit"</span>:</span><br><span class="line"> changes = arguments.get(<span class="string">"changes"</span>) <span class="keyword">if</span> arguments <span class="keyword">else</span> <span class="string">""</span></span><br><span class="line"> <span class="keyword">return</span> types.GetPromptResult(</span><br><span class="line"> messages=[</span><br><span class="line"> types.PromptMessage(</span><br><span class="line"> role=<span class="string">"user"</span>,</span><br><span class="line"> content=types.TextContent(</span><br><span class="line"> <span class="built_in">type</span>=<span class="string">"text"</span>,</span><br><span class="line"> text=<span class="string">f"Generate a concise but descriptive commit message "</span></span><br><span class="line"> <span class="string">f"for these changes:\n\n<span class="subst">{changes}</span>"</span></span><br><span class="line"> )</span><br><span class="line"> )</span><br><span class="line"> ]</span><br><span class="line"> )</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> name == <span class="string">"explain-code"</span>:</span><br><span class="line"> code = arguments.get(<span class="string">"code"</span>) <span class="keyword">if</span> arguments <span class="keyword">else</span> <span class="string">""</span></span><br><span class="line"> language = arguments.get(<span class="string">"language"</span>, <span class="string">"Unknown"</span>) <span class="keyword">if</span> arguments <span class="keyword">else</span> <span class="string">"Unknown"</span></span><br><span class="line"> <span class="keyword">return</span> types.GetPromptResult(</span><br><span class="line"> messages=[</span><br><span class="line"> types.PromptMessage(</span><br><span class="line"> role=<span class="string">"user"</span>,</span><br><span class="line"> content=types.TextContent(</span><br><span class="line"> <span class="built_in">type</span>=<span class="string">"text"</span>,</span><br><span class="line"> text=<span class="string">f"Explain how this <span class="subst">{language}</span> code works:\n\n<span class="subst">{code}</span>"</span></span><br><span class="line"> )</span><br><span class="line"> )</span><br><span class="line"> ]</span><br><span class="line"> )</span><br><span class="line"></span><br><span class="line"> <span class="keyword">raise</span> ValueError(<span class="string">"Prompt implementation not found"</span>)</span><br></pre></td></tr></table></figure><p>上述例子定义了两个 prompt:用于生成 commit message 的 <code>git-commit</code> 和用于解释代码的 <code>explain-code</code> ,它们的具体 prompt 内容都在 <code>get_prompt</code> 中定义,而整体定义是在外面。注意在返回 prompt 列表时,是不会返回具体的 prompt 内容的。</p><p>整体看下来,这个其实非常像函数的定义,prompt 的 name 实际上就是函数名,prompt 的参数就是函数的参数,而函数体实际上就是把参数带进去,生成我们常见的消息格式,role user content 这一套,最终返回该消息。然后在 <code>get_prompt</code> 中,根据不同的 prompt name 返回不同的 prompt 消息。</p><p>所以这样来看,用户是不是就不能更改某个 prompt 的具体内容了?虽然文档上说 prompt 是 user-controlled,但是这里的 control 应该只是选择哪一个 prompt,而不是修改。</p><h2 id="Sampling"><a href="#Sampling" class="headerlink" title="Sampling"></a>Sampling</h2><p>一听到这个词,自然而然想到是 llm 中的采样,事实也是这样的,只是通过 client 实现的采样。根据文档,sampling 可以让 server 通过 client 去请求 llm,流程如下:</p><ol><li>server 向 client 发送一个 <code>sampling/createMessage</code> 请求;</li><li>client 审核请求并可修改它;</li><li>client 从 LLM 中采样,即执行实际的 llm 请求;</li><li>client 审核 completion;</li><li>client 将结果返回给 server。</li></ol><p>server 向 client 发送的请求格式也基本上是遵循了我们常见的 openai 格式:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> messages<span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> role<span class="punctuation">:</span> <span class="string">"user"</span> | <span class="string">"assistant"</span><span class="punctuation">,</span></span><br><span class="line"> content<span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> type<span class="punctuation">:</span> <span class="string">"text"</span> | <span class="string">"image"</span><span class="punctuation">,</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// For text:</span></span><br><span class="line"> text?<span class="punctuation">:</span> string<span class="punctuation">,</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// For images:</span></span><br><span class="line"> data?<span class="punctuation">:</span> string<span class="punctuation">,</span> <span class="comment">// base64 encoded</span></span><br><span class="line"> mimeType?<span class="punctuation">:</span> string</span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line"> modelPreferences?<span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> hints?<span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">{</span></span><br><span class="line"> name?<span class="punctuation">:</span> string <span class="comment">// Suggested model name/family</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line"> costPriority?<span class="punctuation">:</span> number<span class="punctuation">,</span> <span class="comment">// 0-1, importance of minimizing cost</span></span><br><span class="line"> speedPriority?<span class="punctuation">:</span> number<span class="punctuation">,</span> <span class="comment">// 0-1, importance of low latency</span></span><br><span class="line"> intelligencePriority?<span class="punctuation">:</span> number <span class="comment">// 0-1, importance of capabilities</span></span><br><span class="line"> <span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"> systemPrompt?<span class="punctuation">:</span> string<span class="punctuation">,</span></span><br><span class="line"> includeContext?<span class="punctuation">:</span> <span class="string">"none"</span> | <span class="string">"thisServer"</span> | <span class="string">"allServers"</span><span class="punctuation">,</span></span><br><span class="line"> temperature?<span class="punctuation">:</span> number<span class="punctuation">,</span></span><br><span class="line"> maxTokens<span class="punctuation">:</span> number<span class="punctuation">,</span></span><br><span class="line"> stopSequences?<span class="punctuation">:</span> string<span class="punctuation">[</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line"> metadata?<span class="punctuation">:</span> Record<string<span class="punctuation">,</span> unknown></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><p>这些字段基本都和使用 openai 请求时差不多,但有一个不同的是,<code>modelPreferences.hints</code> 中的 <code>name</code> ,这个字段实际上填的是可以匹配完整或部分模型名称的字符串,如 “claude-3” 和 “sonnet”。结合下面的几个字段,client 会自动选择指定的模型,比如最省钱、最快或者最智能。</p><p>一个请求的实际例子:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"method"</span><span class="punctuation">:</span> <span class="string">"sampling/createMessage"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"params"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"messages"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"role"</span><span class="punctuation">:</span> <span class="string">"user"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"content"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"type"</span><span class="punctuation">:</span> <span class="string">"text"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"text"</span><span class="punctuation">:</span> <span class="string">"What files are in the current directory?"</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"systemPrompt"</span><span class="punctuation">:</span> <span class="string">"You are a helpful file system assistant."</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"includeContext"</span><span class="punctuation">:</span> <span class="string">"thisServer"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"maxTokens"</span><span class="punctuation">:</span> <span class="number">100</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><p>响应的格式也和使用 openai 时差不多:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> model<span class="punctuation">:</span> string<span class="punctuation">,</span> <span class="comment">// Name of the model used</span></span><br><span class="line"> stopReason?<span class="punctuation">:</span> <span class="string">"endTurn"</span> | <span class="string">"stopSequence"</span> | <span class="string">"maxTokens"</span> | string<span class="punctuation">,</span></span><br><span class="line"> role<span class="punctuation">:</span> <span class="string">"user"</span> | <span class="string">"assistant"</span><span class="punctuation">,</span></span><br><span class="line"> content<span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"> type<span class="punctuation">:</span> <span class="string">"text"</span> | <span class="string">"image"</span><span class="punctuation">,</span></span><br><span class="line"> text?<span class="punctuation">:</span> string<span class="punctuation">,</span></span><br><span class="line"> data?<span class="punctuation">:</span> string<span class="punctuation">,</span></span><br><span class="line"> mimeType?<span class="punctuation">:</span> string</span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><h2 id="Roots"><a href="#Roots" class="headerlink" title="Roots"></a>Roots</h2><p>Roots 指的是 workspace 的目录,比如根目录、base url 等。这个功能不是很常用,目前似乎只有 gc 支持,其他都不支持,暂且就不做过多介绍了,我也没有什么使用经验。</p><h1 id="END"><a href="#END" class="headerlink" title="END"></a>END</h1>]]></content>
<summary type="html"><p>什么是 mcp transport?什么是 tools?本期更多关注概念。<br></summary>
<category term="LLM" scheme="https://alanlee.fun/tags/LLM/"/>
<category term="MCP" scheme="https://alanlee.fun/tags/MCP/"/>
</entry>
<entry>
<title>MCP 简易指南(一):实践</title>
<link href="https://alanlee.fun/2025/06/03/introducing-mcp-part-1/"/>
<id>https://alanlee.fun/2025/06/03/introducing-mcp-part-1/</id>
<published>2025-06-03T12:00:00.000Z</published>
<updated>2026-04-14T02:51:14.902Z</updated>
<content type="html"><![CDATA[<!-- excerpt --></p><!-- toc --><h1 id="什么是-MCP"><a href="#什么是-MCP" class="headerlink" title="什么是 MCP"></a>什么是 MCP</h1><p>根据<a href="https://modelcontextprotocol.io/introduction">官方文档</a>,mcp 是一种开放协议,用于标准化应用程序向 llm 提供上下文的方式。可以将 mcp 想象成 AI 应用程序的 usb-c 接口。正如 usb-c 提供了一种标准化的方式将设备连接到各种外设和配件,mcp 也提供了一种标准化的方式将 ai 模型连接到不同的数据源和工具。</p><p>总之来说,如果把 llm 比喻成大脑,各种各样的 tools 就是手和脚,mcp 就是让 llm 更方便的获取和切换这些手脚,就像冬兵的枕巾手臂一样,随时可插拔。</p><h1 id="为什么我们需要-MCP"><a href="#为什么我们需要-MCP" class="headerlink" title="为什么我们需要 MCP"></a>为什么我们需要 MCP</h1><p>刚才提到,手和脚是工具。我们可以给一个 llm 编写很多工具给其调用,比如我们可以通过 openai 的 function calling 来赋予 llm 调用工具的能力(虽然本质上执行还是在我们这边,而不是 llm)。此外,我们有很多 llm 提供商,他们的 llm 使用工具的方式或许有所不同。极端情况下,假如我们有 M 个 llm,N 个工具,那么我们就要编写 M×N 个 connector 来让所有 llm 都可以使用这 N 个工具。</p><p>但是如果我们在中间加一层抽象,所有 llm 都通过这层抽象与工具进行交流,那么就只需编写 M+N 个 connector。尤其是对于我们比较终端的开发者来说,我们只需写 N 个,即只写工具即可,M 是在 mcp client 端实现的,我们无需关心如何调用不同的 llm(这就让我想到支持不同的 llm 提供商是多么的繁琐,不过现在已经有了很多库来解决这个事情了)。</p><p>结合下图,如果没有 mcp,即 2,那么每新增一个 llm,我们都需要重新编写 connector 来适配这 3 个工具。</p><p>所以 mcp 的最大作用,就是减少重复工作量。但是另一方面,如果全部 llm 都支持 openai 的 function calling 格式,那么确实我们无需 mcp,也没有重复工作。这就是标准之争了。</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2025/06/03/ZMt6VbwiA2B831v.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2025/06/03/ZMt6VbwiA2B831v.png" alt=""></a></div><h1 id="核心概念"><a href="#核心概念" class="headerlink" title="核心概念"></a>核心概念</h1><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2025/06/03/GhYTJo59fbMeHtS.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2025/06/03/GhYTJo59fbMeHtS.png" alt=""></a></div><ul><li>mcp host:host 就是用户与 mcp 的交互界面,任何支持调用 mcp server 的 app 都是 host。比如 github copilot for vscode、claude desktop。</li><li>mcp client:client 是真正与 server 交互的部分,一般在 host 中实现。可能大多数情况并不自己实现。</li><li>mcp server:这就是真正实现 mcp 逻辑的地方,在这里你会实现你需要的 tools,供 host 和 client 调用。</li></ul><h1 id="安装-mcp-python-sdk"><a href="#安装-mcp-python-sdk" class="headerlink" title="安装 mcp python sdk"></a>安装 mcp python sdk</h1><p><a href="https://modelcontextprotocol.io/quickstart/server#set-up-your-environment">官方文档</a>推荐使用 <code>uv</code> 安装环境和依赖:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 初始化项目,这会自动创建目录,并生成一些必要文件。</span></span><br><span class="line">uv init project_name</span><br><span class="line">cd project_name</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 安装依赖,这会将环境安装在当前目录的 .venv/ 下,</span></span><br><span class="line"><span class="comment"># 并将依赖写入 pyproject.toml。</span></span><br><span class="line">uv add <span class="string">"mcp[cli]"</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 激活环境,这会让 python 命令指向当前环境的 python 版本。</span></span><br><span class="line">source .venv/<span class="built_in">bin</span>/activate</span><br></pre></td></tr></table></figure><p>如果你对 <code>uv</code> 不熟,或者想要使用 conda/pip,那么你仍然可以使用 pip 安装:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install <span class="string">"mcp[cli]"</span></span><br></pre></td></tr></table></figure><h1 id="如何编写和使用-mcp-server"><a href="#如何编写和使用-mcp-server" class="headerlink" title="如何编写和使用 mcp server"></a>如何编写和使用 mcp server</h1><blockquote><p>这里以编写 mcp tools 为例,大部分情况我们写的都是 tools。</p></blockquote><p>下面是一个简单的用于获取当前时间的 mcp server 例子:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> datetime <span class="keyword">import</span> datetime</span><br><span class="line"><span class="keyword">from</span> zoneinfo <span class="keyword">import</span> ZoneInfo</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> mcp.server.fastmcp <span class="keyword">import</span> FastMCP</span><br><span class="line"></span><br><span class="line"><span class="comment"># Create an MCP server</span></span><br><span class="line">mcp = FastMCP(<span class="string">"Demo MCP Server"</span>)</span><br><span class="line">mcp.settings.host = <span class="string">"0.0.0.0"</span></span><br><span class="line">mcp.settings.port = <span class="number">7011</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@mcp.tool()</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">get_current_time</span>():</span><br><span class="line"> <span class="string">"""</span></span><br><span class="line"><span class="string"> Returns the current date and time in the 'Asia/Shanghai' timezone.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string"> Returns:</span></span><br><span class="line"><span class="string"> datetime: The current datetime object localized to 'Asia/Shanghai' timezone.</span></span><br><span class="line"><span class="string"> """</span></span><br><span class="line"> <span class="keyword">return</span> datetime.now(tz=ZoneInfo(<span class="string">"Asia/Shanghai"</span>))</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">"__main__"</span>:</span><br><span class="line"> <span class="comment"># Start the server</span></span><br><span class="line"> mcp.run(transport=<span class="string">"streamable-http"</span>)</span><br></pre></td></tr></table></figure><p>语法是不是看起来很熟悉,基本和 flask、fastapi 一致。整体流程就这么三步:</p><ol><li>创建 mcp 对象:<code>mcp = FastMCP("Demo MCP Server")</code> ;</li><li>编写 tools 函数,用 <code>@mcp.tool()</code> 进行装饰;</li><li>启动 server:<code>mcp.run(transport="streamable-http")</code> ,这里的 transport 先不用关心,下面会再说,暂时你就认为这是一个 http 服务即可。</li></ol><p>运行后,你的 mcp 地址就是 <code>http://localhost:7011/mcp</code> ,将这个地址添加到你的 host 中即可。下面我以 github copilot in vscode 为例,演示下如何添加 mcp server:</p><ol><li><p>command + shift + p 调出命令中心,输入 <code>mcp add</code> ,选择添加服务器 → http:</p> <div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2025/06/03/UdJX4sQVvhMaA1i.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2025/06/03/UdJX4sQVvhMaA1i.png" alt=""></a></div> <div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2025/06/03/Onx5wLmyRSX62zb.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2025/06/03/Onx5wLmyRSX62zb.png" alt=""></a></div></li><li><p>按照提示填入 url(就是刚才的地址,但是要记得在后面以斜杠结尾,详见下面的 gotcha)、name,然后就可以看到添加好的 json 配置,已经正常启动了,也已经发现了 1 个工具,就是刚才的获取当前时间的工具:</p> <div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2025/06/03/FnpLkrOb7wmsv51.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2025/06/03/FnpLkrOb7wmsv51.png" alt=""></a></div></li><li><p>打开 gc,切换到 agent 模式,因为只有在 <code>agent</code> 模式下才能使用 mcp:</p> <div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2025/06/03/kO6wFGA4MqLuK5c.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2025/06/03/kO6wFGA4MqLuK5c.png" alt=""></a></div></li><li><p>你可以询问“现在几点了”等和时间有关的问题,gc 会提示可以运行 mcp tools 来获取时间,你选择“继续”即可:</p> <div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2025/06/03/EdiZxbqGO3yIDFz.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2025/06/03/EdiZxbqGO3yIDFz.png" alt=""></a></div></li></ol><p>另外点击扳手图标可以看到可用的工具,包含我们刚才添加的 mcp server:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2025/06/03/mSD4UrPol23LRJV.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2025/06/03/mSD4UrPol23LRJV.png" alt=""></a></div><h2 id="调试"><a href="#调试" class="headerlink" title="调试"></a>调试</h2><p>当你写好一个 server 和 tools 之后,你需要测试一下是否能正常跑通。mcp 官方提供了 <a href="https://github.com/modelcontextprotocol/inspector">mcp inspector</a> 用于调试,此外也有很多其他的测试工具。我现在常用的是 postman,对,就是那个调试接口的 postman,它现在也支持 mcp 了,甚至还支持直接调用 llm 😂:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2025/06/03/OhTkonNrf3piEl6.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2025/06/03/OhTkonNrf3piEl6.png" alt=""></a></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2025/06/03/QhJYXqIFPRvs3GB.png" title="postman" data-caption="postman" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2025/06/03/QhJYXqIFPRvs3GB.png" alt="postman"></a><span class="caption">postman</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2025/06/03/o34ezQSTih5vNHG.png" title="mcp inspector" data-caption="mcp inspector" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2025/06/03/o34ezQSTih5vNHG.png" alt="mcp inspector"></a><span class="caption">mcp inspector</span></div><p>用起来比 inspector 方便点,inspector 还得启动一下浏览器里打开,而且界面有点乱的感觉:</p><p>ok,以上基本就是使用层面,如果你的目的是编写一个 mcp server 来提供 tools,那么基本已经够了。如果你还想了解更多一点,可以看下一期,为了保持文章简短集中,我就不放在这里了。</p><h1 id="Gotchas"><a href="#Gotchas" class="headerlink" title="Gotchas"></a>Gotchas</h1><ol><li>在 vscode 中添加 server 时,streamable http(<code>http</code>)类型的 server 的 URL 必须以斜杠 <code>/</code> 结尾,否则会报错:<code>Error sending message to http://localhost:7011/mcp: TypeError: fetch failed</code> ,server 日志显示:<code>INFO: ip:40226 - "POST /mcp HTTP/1.1" 307 Temporary Redirect</code> ,通过 <code>curl -v -X POST http://your-server/mcp</code> 可以看到,重定向到了斜杠结尾的路由。但是使用 inspector 或者 postman 时,仍然可以直接使用非斜杠结尾的 URL。</li></ol><h1 id="References"><a href="#References" class="headerlink" title="References"></a>References</h1><ul><li><a href="https://wandb.ai/onlineinference/mcp/reports/The-Model-Context-Protocol-MCP-by-Anthropic-Origins-functionality-and-impact--VmlldzoxMTY5NDI4MQ#mcp-vs.-existing-context-management-methods">The Model Context Protocol (MCP) by Anthropic: Origins, functionality, and impact | mcp – Weights & Biases</a></li><li><a href="https://www.youtube.com/watch?v=7j_NE6Pjv-E">Model Context Protocol (MCP), clearly explained (why it matters) - YouTube</a></li><li><a href="https://www.digitalocean.com/community/tutorials/model-context-protocol">MCP 101: An Introduction to Model Context Protocol | DigitalOcean</a></li></ul><h1 id="END"><a href="#END" class="headerlink" title="END"></a>END</h1>]]></content>
<summary type="html"><p>什么是 mcp?为什么我们需要 mcp?如何使用?本期更多关注实践,而非概念。<br></summary>
<category term="LLM" scheme="https://alanlee.fun/tags/LLM/"/>
<category term="MCP" scheme="https://alanlee.fun/tags/MCP/"/>
</entry>
<entry>
<title>我的 2024</title>
<link href="https://alanlee.fun/2024/12/28/my-2024/"/>
<id>https://alanlee.fun/2024/12/28/my-2024/</id>
<published>2024-12-28T09:10:00.000Z</published>
<updated>2026-04-14T02:51:14.903Z</updated>
<content type="html"><![CDATA[<!-- excerpt --><!-- toc --><p>2024 马上就要过去了,真的好快!</p><p>不知道从什么时候开始,我也开始有每年底写一个年度回顾的想法。哦对了,应该是从网易云音乐的年度回顾开始。</p><p>回想这一年来发生了什么,有哪些收获,有哪些感想,获得了什么,失去了什么。从哪里开始呢?就从最大的事情开始吧。</p><h1 id="房子"><a href="#房子" class="headerlink" title="房子"></a>房子</h1><p>房子属实是意外,本没有打算今年买房,但是奈何政策变化太快了,各种利好政策纷至沓来,然后就看了看,然后就买了 😂。具体可以看我之前的写的<a href="https://alanlee.fun/2024/11/03/my-home-purchase-story/">买房记</a>,在这里就不赘述了。</p><h1 id="孩子"><a href="#孩子" class="headerlink" title="孩子"></a>孩子</h1><p>今年迎来了我们的第二个孩子,这个确实是计划之中。我觉得两个孩子是最佳的状态,既不多也不少,1 个孩子略显孤单,3 个孩子略显嘈杂。</p><p>但是生第二个孩子的代价,是很大的,尤其是像我们这种两个孩子相差 3 岁的情况。俗话说,terrible 2 horrible 3。两三岁这个年龄的孩子,精力极其旺盛,探索欲极其之强,总想搞点大事情,在家里根本闲不住。然而这时候家里迎来一个刚出生的孩子时,需要一个安静的环境,这就造成了冲突和矛盾。这种冲突简直会让你崩溃,我们家还是父母在帮忙照看的情况,如果没有父母照顾,简直难以度过这段艰难的时期。</p><p>说到这里,在 2 岁多的时候,让孩子去上托班,是一个不错的选择。这样几乎可以避免上述冲突。但是恰巧我们想让老大去上托班的时候,正是冬季。然后冬季是流感高发期,去上托班就意味着大概率会生病,然后将病毒带回家,然后传给老二,这是我们非常不愿意看到的。所以还是选择年后送去托班。</p><p>总之,想要生老二,最好让两个孩子年龄避开 3 岁这个尴尬的差距。将来两个孩子有可能同时处于升学关键期:中考和高考。</p><h1 id="车子"><a href="#车子" class="headerlink" title="车子"></a>车子</h1><p>北京的摇号政策是在太恶心,2024 年 第二期摇号意料之中的落空,320 万申请者(包括家庭和个人),9600 个指标,粗略算下来,平均摇中概率只有 0.3%。排新能源车也是遥遥无期,model y 算是这两年买不到了,不过 2025 年要出新款了,所以算是塞翁失马?</p><p>四轮车买不了,两轮车还是可以买的。在搬家前那段时间,买了一辆九号 F90。这辆小车,在搬家前后可是帮了大忙,省了不少打车钱。自从有了它,后来打车次数直线下降,短途出行全靠它。后来天气渐冷,才逐渐开始打车。不得不说,春秋季,简直是电动车的黄金时期,骑着车吹风太舒服了。</p><h1 id="牛子"><a href="#牛子" class="headerlink" title="牛子"></a>牛子</h1><p>什么是牛子?牛马……哈哈哈,只是为了强行与上面对齐,又不能叫马子,只能叫牛子了。我是做 NLP 的,但模型工作在今年只占一小部分,今年的很大部分工作,都是工程上的问题,有很多我之前我没接触过或者了解不深的东西,比如 Kafka、redis 以及 ToC 产品的生产代码编写和性能监控。</p><p>与此同时,LLM 大爆发,无论是国内还是国外,此起彼伏。LLM 的使用门槛越来越低,价格越来越便宜,对 NLP 工程师可能是个挑战。以前我们训练一个分类模型,可能真的需要 NLP 工程师的参与,毕竟编写处理数据和训练代码等还是有一定门槛的。但是现在,只要你把类别列表、待分类文本扔给 LLM,一次 API 调用,就可以得到一个不错的结果,如果你用的是 gpt-4o 等高阶模型,那么你的效果更不会太差,你甚至都不需要微调(而且微调成本较高,效果也不一定好)。所以说你只需一个传统的 Java 工程师即可,甚至是一个实习生!<strong>平权正在发生!</strong></p><p>但是话说回来,如果你是调用 LLM API 来使用,那么在发生分类错误时,即 LLM 始终对一些文本分错类,那么此时你基本无法去优化,你能做的只有调整 prompt 或等待模型升级。如果你使用的是本地部署的 LLM,那么要么较小模型效果不好,要么较大模型成本较高,微调费时费力,还不一定有提升。而使用“传统”模型(是的,BERT 这种现在也只能说是传统了),你有更为灵活的模型优化方法和更快的推理速度。对于摘要等生成任务,我认为目前最佳做法仍然是 LLM,但是注意幻觉问题,强如 Apple,也会闹出胡说八道的笑话。</p><p>下一年希望继续增强自己的工程能力和算法能力,阅读更多的论文,理解更多的概念,写出更多的博客!</p><h1 id="野子"><a href="#野子" class="headerlink" title="野子"></a>野子</h1><p>即牛子之外的探索。今年探索了几个项目,主要是:</p><ul><li><a href="https://github.com/secsilm/video-translator">Video Translator</a>:上传一个视频,自动加上中文字幕。这个其实没啥核心技术含量,主要就是 ffmpeg + whisper。希望接下来加上 GitHub 自动打包发布(docker),使其更易用。</li><li>声音克隆:这个目前还未公开。这也是我第一次实际训练音频模型,使用的是 xtts_v2,声音是柴知道之前的女性声音,使用了大概 5 分钟的声音,走了一下从音频获取到模型部署的全流程,收获不少。虽然 xtts 本身支持直接根据 reference audio 进行克隆,但是训练过后发现效果更好。 <div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2024/12/28/IuU3js5RlfMEaCH.jpg" title="从音频获取到模型部署全流程" data-caption="从音频获取到模型部署全流程" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2024/12/28/IuU3js5RlfMEaCH.jpg" alt="从音频获取到模型部署全流程"></a><span class="caption">从音频获取到模型部署全流程</span></div></li><li><a href="https://github.com/secsilm/til">til</a>:记录日常遇到的不足以形成一篇完整博文的小问题、小技术点,实际上就是小笔记,之前我的笔记都在 GitHub 和 Notion 上。灵感来自 <a href="https://github.com/simonw/til">simonw/til</a> 。希望新的一年继续坚持!</li><li>博客自动发布。利用 GitHub Action 的能力,在 GitHub 上直接编辑博客,提交之后执行 action,build and deploy。详见<a href="https://alanlee.fun/2024/07/05/deploy-hexo-with-github-action/">使用 GitHub Actions 自动发布 Hexo 博客</a>。</li></ul><h1 id="乐子"><a href="#乐子" class="headerlink" title="乐子"></a>乐子</h1><p>根据豆瓣统计,今年共看过 79 部影视。直观上感觉这个数量明显低于去年,主要还是今年太忙了。而且今年国内上映的感兴趣的电影不多。</p><p><a href="https://movie.douban.com/subject/35575567/">沙丘 2</a>、<a href="https://movie.douban.com/subject/35244032/">洛基 2</a>、<a href="https://movie.douban.com/subject/30291334/">幕府将军</a>、<a href="https://movie.douban.com/subject/35750081/">美国内战</a>、<a href="https://movie.douban.com/subject/36189205/">凡人歌</a>、<a href="https://movie.douban.com/subject/36439490/">异教徒</a>、<a href="https://movie.douban.com/subject/30169926/">Devs</a>、<a href="https://movie.douban.com/subject/35819414/">人生复本</a>、 <a href="https://movie.douban.com/subject/34963448/">终极名单</a>、<a href="https://movie.douban.com/subject/30378897/">Reacher</a> 和<a href="https://movie.douban.com/subject/36323224/">豺狼的日子</a>等,是值得点出来的,后三个(<code>[-3:]</code>),我称之为“<a href="https://www.douban.com/doulist/160563962/">孤胆硬汉大杀四方</a>”,只不过硬汉的风格不同。</p><p><code>[-4:-6]</code> 是非常不错的关于平行宇宙和薛定谔的猫的片子,Devs 中的理念:Everything is predictable,在当下 LLM 盛行的年代,岂不就是 AGI 的另一种表达?我也认同这句话,有些东西我们现在看起来是不可预测的,但是否是我们对其了解的太少?特征不够多?</p><h1 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h1><p>回顾 2024 年,买房和老二出生是大事,生活上忙忙碌碌,工作上也忙忙碌碌,技术上也有所探索和收获,但博客发表量太少,太多仍在草稿阶段。希望 2025 年继续提升技术,继续推进现有的一些个人项目,发表更多的博客!<strong>Don’t think. Just do it!</strong></p><p>Happy coding, Happy blogging, Happy NEW YEAR!</p><h1 id="END"><a href="#END" class="headerlink" title="END"></a>END</h1>]]></content>
<summary type="html"><p>2024 年即将落幕,这一年,你过得如何?从意外买房到迎来二宝,从技术探索到工作挑战,从电影乐趣到个人成长——我的 2024,忙碌而充实。让我们一起回顾这一年的点滴,展望更精彩的 2025![ChatGPT 4o]</p></summary>
<category term="Living" scheme="https://alanlee.fun/tags/Living/"/>
<category term="Recap" scheme="https://alanlee.fun/tags/Recap/"/>
</entry>
<entry>
<title>买房记</title>
<link href="https://alanlee.fun/2024/11/03/my-home-purchase-story/"/>
<id>https://alanlee.fun/2024/11/03/my-home-purchase-story/</id>
<published>2024-11-03T12:34:00.000Z</published>
<updated>2026-04-14T02:51:14.904Z</updated>
<content type="html"><![CDATA[<!-- excerpt --><!-- toc --><p>2024 年以来,全国房价都不太太平,跌一直都是主旋律,政府也在想尽办法稳住放假,一系列政策纷纷出台:取消或者放松限购、降利率(一年内降 6 次)、取消加点(甚至是减点,包括呼声很高的存量房贷),甚至最后降首付比例(北京也能到 15%),可以说是比较有诚意了。</p><p>北京也逃不过。北京的政策按照五环内和五环外进行区分,利率和首付比例等政策都不太相同,具体可以看后面的政策部分。起初的一系列政策效果有点差强人意,但是 930 新政出台,放松限购以及降首付比例和房贷利率,北京的楼市在 10 月一下热了起来。</p><p>对于购房者来说,尤其是刚需,其实是利好,我们只打算住,不打算炒,价格越低越好。但是这价格什么时候是个底,谁也说不准。很多人要等到最低点才买,实际上我觉得没必要,未来谁也说不好,只要你觉得不亏,可以接受,那就可以入了,哪能一点亏都不吃呢?正所谓早买早享受,晚买享折扣。</p><p>其实我本来也没打算买房,但是一直在关注房价。我家那位倒是有买房的想法,然后某个周末就约了中介看房。这个中介是我们1年前租房的时候加的微信,当时他们为了完成带看任务,硬是带我们去看了一两套房,那也算是我们第一次看房吧哈哈哈。</p><p>我们在租住的小区已经住了 4 年左右,已经很习惯了,户型也大致了解,但无奈价格太高,即使勉强够着,压力也比较大,所以基本不打算买这里。然后看了看周边的和新城的一些房。整个看了几套下来,我们对自己的需求认识也越来越深刻,越拉越明晰:</p><ol><li>三居。</li><li>八九十平以上。</li><li>不用大装修(我媳妇快生了)。</li><li>采光要好。</li><li>首付控制在 X 十万以内。</li></ol><h1 id="看房"><a href="#看房" class="headerlink" title="看房"></a>看房</h1><p>总共其实看了也就 10 套房左右,小区的话其实也就 3 个小区左右。因为拿着上面的需求去筛,根本没有多少符合的。</p><p>首先是我们当时租住的小区,小区环境和条件都还不错,物业也靠谱,但是价格有点高,当时要买的话只能买两居的,所以就只能放弃了。</p><p>然后看了附近的一个小区,这个小区比较小,总共就只有大概三四栋楼,似乎是和旁边商品房一起配套建的回迁房。看了看感觉户型不咋样,也就放弃了。唯一的优点是海拔高点,去年发大水的时候基本不受影响。</p><p>从这里出来后,中介提议我们去看另一个房子,当时也没事,就跟着去了,这套房子就是大家所说的手枪房。大家都说手枪房不好,但是我们实地看了一下后,觉得还是可圈可点的,户型图大致如下:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2024/11/03/xLvY4MsHgDdUJF5.jpg" title="手枪房户型图" data-caption="手枪房户型图" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2024/11/03/xLvY4MsHgDdUJF5.jpg" alt="手枪房户型图"></a><span class="caption">手枪房户型图</span></div><p>主卧朝南,客厅和次卧朝北,枪管部分确实很长,但是宽度足够,业主给那一整面墙都打了柜子,储物空间超大。客厅面积也很大,虽然朝北,但是由于高层,并不显得暗。反而进来之后有一种宽阔感。可能唯一不足的是餐厅与厨房距离过远。对我们来说,确实感觉不错,但是由于是两居,所以后来没怎么看了。这个小区整体上是一个商住小区,但是中间有两栋楼却是住宅。这套房子就是在这两栋住宅中。不得不说,商住小区的环境确实挺好的。</p><p>从这里出来后,又接着去看了附近的一个新房项目,还在建设中,看了看样板房。中介似乎和他们有合作,应该带一个客户过来多少提成吧。看了看后,觉得还真不错,把 89 平空间利用到了极致,三居,精装交付,看着很舒服,四叶草户型,客厅两个卧室都有飘窗,零冷水设计。但就是价格还是有点贵,而且位置偏了一点,还是在山底斜坡上。鉴于去年的大水,我们也就放弃了,不过我们还是挺心动的。</p><p>最后就是看的我们现在买的这个小区的。这个小区主户型是一个南北通透的 88 平三居,还有南向三居和两居的。这个小区公摊大,电梯口太宽敞,比我们租住的小区宽敞多了。我们看了看几个户型,最终选定南北通透三居的。由于我们马上会迎来家庭新成员,所以希望找一个装修好点的,我们住进去后不用大装的,后来终于等到一个,虽然楼层低,但是也能接受了(起初我们一直想找一个中楼层的)。</p><h1 id="交易"><a href="#交易" class="headerlink" title="交易"></a>交易</h1><p>买房之前不知道,这流程也太复杂了,约谈(谈了 4 个多小时)、意向合同、定金、网签、过户等,按照流程一步一步走。其他点我就不细说了,我要特别说一点,这点造成了太多麻烦。</p><p>业主除了这一套房,还有一套房,那就是满五不唯一了,是要交税什么的,而且金额不小。中介给他们出了一个主意:先把房子<strong>赠予</strong>给他们孩子(孩子很小),然后以孩子的名义卖房,而当时在我们这,这种情况可以视为满五唯一。中介说他们之前这么操作过,但是业主心里可没底,而且办理赠予过户也是需要费用的,大概 1.5 万吧,而且这中间大概需要两周时间,又费时间又费钱的。</p><p>所以当初约谈确定价格的时候,没有签定金合同(需要交定金 10 万),因为大家都不确定最后到底能否唯一成功,只是签了一个没有定金的合同。</p><p>最后好不容易唯一成功了,业主又觉得当时价格低了,想反悔(当时男业主来谈的,但实际定价人是他老婆),我估计就是因为当时没签定金合同,他们才敢这么说。我不是那种喜欢和对方拉扯的人,而且价格当时确实低于我们预期较多,我当时就说最多加 1 万,相当于是我们出了大部分赠予过户的钱,这就是我们的 final price 了,如果这不行,那就算了。最终他们同意,继续交易。</p><p>接着就是谈家具去留的问题,令我们惊讶的是,和当初定价一样,男业主说的话,全部被女业主推翻,一切重来……真的是服了。这段又是拉扯很久,真的心累。不过中介说这业主还不算最夸张的,他们见过把燃气灶都拆走的 🤣。</p><p>最后首付构成如下:</p><ul><li>净首付:首付 + 定金(10 万)+ 物业保证金(5 万)+ 户口保证金(5 万)。</li><li>契税:总价 1%。</li><li>中介费:总价 1%。</li></ul><p>附上我们的买房时间线:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2024/11/09/PN3FTZoz7vVbeHW.jpg" title="贝壳上记录的时间线" data-caption="贝壳上记录的时间线" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2024/11/09/PN3FTZoz7vVbeHW.jpg" alt="贝壳上记录的时间线"></a><span class="caption">贝壳上记录的时间线</span></div><h1 id="政策"><a href="#政策" class="headerlink" title="政策"></a>政策</h1><p>最后说下政策变化。前面也说过,本来我没打算今年买房(但是政策是一直在关注),看房只是随便看看,但是我媳妇买房的意愿比较强一点,再加上最近的政策是越来越好,越来越偏向买方市场,各种降利率、降首付等,所以我也有点想下手的感觉。总有人说等一等,房价会更低,但是站在当下,你不太可能踩在最低点买房,不寻求全局最优,局部最优即可,降低我们能接受的价位即可。</p><p>这一两年,国家的房地产政策可谓是一波接一波,其他地方我就不说了,仅说下北京,这里引用一下京房字公众号的<a href="https://mp.weixin.qq.com/s/bZ0fOfeMgEdxF-Bim2RCaA">一篇文章</a>中的梳理:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2024/11/03/ryxO1MXJlVm8ujK.png" title="北京买房首付比例变化" data-caption="北京买房首付比例变化" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2024/11/03/ryxO1MXJlVm8ujK.png" alt="北京买房首付比例变化"></a><span class="caption">北京买房首付比例变化</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2024/11/03/WZFKdtGbq1Q9eDy.png" title="北京买房商贷利率变化" data-caption="北京买房商贷利率变化" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2024/11/03/WZFKdtGbq1Q9eDy.png" alt="北京买房商贷利率变化"></a><span class="caption">北京买房商贷利率变化</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2024/11/03/8Kj4seSkUPaWwQt.png" title="LPR 也是多次调整" data-caption="LPR 也是多次调整" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2024/11/03/8Kj4seSkUPaWwQt.png" alt="LPR 也是多次调整"></a><span class="caption">LPR 也是多次调整</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2024/11/03/P3LdRqAFbXpn49y.png" title="公积金贷款利率调整,五年期以上**首套房贷利率降至 2.85%,二套房贷利率降至 3.325%**。" data-caption="公积金贷款利率调整,五年期以上**首套房贷利率降至 2.85%,二套房贷利率降至 3.325%**。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2024/11/03/P3LdRqAFbXpn49y.png" alt="公积金贷款利率调整,五年期以上**首套房贷利率降至 2.85%,二套房贷利率降至 3.325%**。"></a><span class="caption">公积金贷款利率调整,五年期以上**首套房贷利率降至 2.85%,二套房贷利率降至 3.325%**。</span></div><p>其他政策我就不细说了,大家可以参考上述公众号文章,说的挺全面了。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>买房确实是一个大事情,起初我觉得把价格谈定就好,所以第一次约谈时由于我在出差,就让我媳妇顶着大肚子去谈了,实在是不应该。后面的沟通也是十分费心,再加上搬家、添置家具等,弄完也十分疲劳了。世上哪有那么十全十美的事情,重要的是要调整好自己的心态。</p><p>Shitty World, Happy Life!</p><h1 id="END"><a href="#END" class="headerlink" title="END"></a>END</h1>]]></content>
<summary type="html"><p>面对房价下跌与政策支持,我们在北京多方看房后决定入手。选择一套适合的三居,交易过程复杂却最终成功落地。[ChatGPT 4o]</p></summary>
<category term="Living" scheme="https://alanlee.fun/tags/Living/"/>
</entry>
<entry>
<title>记一个韩文字符规范化的坑</title>
<link href="https://alanlee.fun/2024/11/02/korean-unicode-normalization/"/>
<id>https://alanlee.fun/2024/11/02/korean-unicode-normalization/</id>
<published>2024-11-02T14:49:00.000Z</published>
<updated>2026-04-14T02:51:14.903Z</updated>
<content type="html"><" alt=""></a></div><div class="alert info"><p><code>compatibility</code> 这个单词中明明没有 K,那为什么是 KD 和 KC 呢?这是为了避免 compatibility 的 C 与 canonical 和 composition 的 C 混淆,所以用了 K 来指代 compatibility。</p></div><p>分解方式有两种:canonical decomposition 和 compatibility decomposition,也就是有无 K 的区别。两种分解方式都是将字符串分解为他们的等价形式,前者的分解结果,在外观上尽量与原始字符串一致(这里的一致指的是在字符串被正确渲染时,看起来是一样的),语义上也是一致的。而后者则是忽略外观,只找到语义上等价的字符串。典型的兼容分解有:</p><ul><li>全角转半角。</li><li>上下标转成普通数字字母,比如上标 $^2$ 转成普通的 2。</li></ul><p>简单来说,带 C 的都多一个组合的步骤,带 D 的都只有分解步骤,带 K 的都会进行兼容性处理(比如全角转半角、上下标转为对应的字母数字)。</p><p>以下是几个示例:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line">In [<span class="number">24</span>]: <span class="keyword">def</span> <span class="title function_">normalize</span>(<span class="params">text: <span class="built_in">str</span></span>):</span><br><span class="line"> ...: res = unicodedata.normalize(<span class="string">'NFC'</span>, text)</span><br><span class="line"> ...: <span class="built_in">print</span>(<span class="string">f"NFC: <span class="subst">{res!r}</span>, len=<span class="subst">{<span class="built_in">len</span>(res)}</span>"</span>)</span><br><span class="line"> ...: res = unicodedata.normalize(<span class="string">'NFD'</span>, text)</span><br><span class="line"> ...: <span class="built_in">print</span>(<span class="string">f"NFD: <span class="subst">{res!r}</span>, len=<span class="subst">{<span class="built_in">len</span>(res)}</span>"</span>)</span><br><span class="line"> ...: res = unicodedata.normalize(<span class="string">'NFKC'</span>, text)</span><br><span class="line"> ...: <span class="built_in">print</span>(<span class="string">f"NFKC: <span class="subst">{res!r}</span>, len=<span class="subst">{<span class="built_in">len</span>(res)}</span>"</span>)</span><br><span class="line"> ...: res = unicodedata.normalize(<span class="string">'NFKC'</span>, text)</span><br><span class="line"> ...: <span class="built_in">print</span>(<span class="string">f"NFKD: <span class="subst">{res!r}</span>, len=<span class="subst">{<span class="built_in">len</span>(res)}</span>"</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 全角 1</span></span><br><span class="line">In [<span class="number">25</span>]: normalize(<span class="string">'1'</span>)</span><br><span class="line">NFC: <span class="string">'1'</span>, <span class="built_in">len</span>=<span class="number">1</span></span><br><span class="line">NFD: <span class="string">'1'</span>, <span class="built_in">len</span>=<span class="number">1</span></span><br><span class="line">NFKC: <span class="string">'1'</span>, <span class="built_in">len</span>=<span class="number">1</span></span><br><span class="line">NFKD: <span class="string">'1'</span>, <span class="built_in">len</span>=<span class="number">1</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 下标 m</span></span><br><span class="line">In [<span class="number">26</span>]: normalize(<span class="string">'ₘ'</span>)</span><br><span class="line">NFC: <span class="string">'ₘ'</span>, <span class="built_in">len</span>=<span class="number">1</span></span><br><span class="line">NFD: <span class="string">'ₘ'</span>, <span class="built_in">len</span>=<span class="number">1</span></span><br><span class="line">NFKC: <span class="string">'m'</span>, <span class="built_in">len</span>=<span class="number">1</span></span><br><span class="line">NFKD: <span class="string">'m'</span>, <span class="built_in">len</span>=<span class="number">1</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 圆圈 1</span></span><br><span class="line">In [<span class="number">27</span>]: normalize(<span class="string">'①'</span>)</span><br><span class="line">NFC: <span class="string">'①'</span>, <span class="built_in">len</span>=<span class="number">1</span></span><br><span class="line">NFD: <span class="string">'①'</span>, <span class="built_in">len</span>=<span class="number">1</span></span><br><span class="line">NFKC: <span class="string">'1'</span>, <span class="built_in">len</span>=<span class="number">1</span></span><br><span class="line">NFKD: <span class="string">'1'</span>, <span class="built_in">len</span>=<span class="number">1</span></span><br></pre></td></tr></table></figure><h1 id="韩语怎么特殊了"><a href="#韩语怎么特殊了" class="headerlink" title="韩语怎么特殊了"></a>韩语怎么特殊了</h1><p>我们再来看两个例子:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2024/11/02/oXuzUxJl182YtZB.png" title="" data-caption="" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2024/11/02/oXuzUxJl182YtZB.png" alt=""></a></div><p>前面不是说了尽量保持外观一致吗?怎么韩文这里看起来完全变了呢?下面的 e 就没问题(虽然实际上也是被分解成了两个字符)。那是因为:</p><blockquote><p>There are also special rules to fully decompose Hangul syllables.</p></blockquote><p>韩文就是这么特殊,韩文是由音节组成的,没错,就是类似我们的拼音,所以他们是直接用拼音写字的。具体来说,<code>가</code> 是一个<strong>预组合</strong>的韩文字符,由以下部分组成:</p><ul><li><strong>初声</strong>:ᄀ (U+1100, HANGUL CHOSEONG KIYEOK)</li><li><strong>中声</strong>:ᅡ (U+1161, HANGUL JUNGSEONG A)</li></ul><p>所以会看到被分开了。但是如果你把输出复制到网页上,也就是 <code>'가'</code> ,你就又发现,看起来好像又一致了,这就是上面说的渲染的问题。</p><p>而韩文的 NFKD 和 NFD 的结果是一样的,所以就导致所有韩文都被分解成他们的音节了,导致 embedding 模型出错,几乎全都判成相似了。比如下面的两句话:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">sents = [</span><br><span class="line"> <span class="string">'오늘 날씨가 좋다'</span>, <span class="comment"># 今天天气很好</span></span><br><span class="line"> <span class="string">'나는 영화를 보러 가고 싶다'</span> <span class="comment"># 我想去看电影</span></span><br><span class="line">]</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f"lens: <span class="subst">{<span class="built_in">list</span>(<span class="built_in">map</span>(<span class="built_in">len</span>, sents))}</span>"</span>)</span><br><span class="line">embeddings = model.encode(sents)</span><br><span class="line"><span class="built_in">print</span>(cos_sim(embeddings, embeddings))</span><br><span class="line"></span><br><span class="line"><span class="comment"># lens: [9, 15]</span></span><br><span class="line"><span class="comment"># tensor([[1.0000, 0.1595],</span></span><br><span class="line"><span class="comment"># [0.1595, 1.0000]])</span></span><br><span class="line"></span><br><span class="line">normalized_sents = [unicodedata.normalize(<span class="string">'NFD'</span>, sent) <span class="keyword">for</span> sent <span class="keyword">in</span> sents]</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f"lens: <span class="subst">{<span class="built_in">list</span>(<span class="built_in">map</span>(<span class="built_in">len</span>, normalized_sents))}</span>"</span>)</span><br><span class="line">embeddings = model.encode(normalized_sents)</span><br><span class="line"><span class="built_in">print</span>(cos_sim(embeddings, embeddings))</span><br><span class="line"></span><br><span class="line"><span class="comment"># lens: [19, 30]</span></span><br><span class="line"><span class="comment"># tensor([[1.0000, 0.9953],</span></span><br><span class="line"><span class="comment"># [0.9953, 1.0000]])</span></span><br></pre></td></tr></table></figure><p>可以看到原始句子的相似度很低,这是正常的,然而 <strong>NFD 规范化后,相似度都快到 1 了</strong>。</p><h1 id="References"><a href="#References" class="headerlink" title="References"></a>References</h1><ul><li><a href="https://www.unicode.org/reports/tr15/">UAX #15: Unicode Normalization Forms</a></li><li><a href="https://www.unicode.org/faq/normalization.html">FAQ - Normalization</a></li></ul><h1 id="END"><a href="#END" class="headerlink" title="END"></a>END</h1>]]></content>
<summary type="html"><p>在多语言文本处理时,作者因使用 NFKD 规范化导致韩文字符被过度分解,影响了文本分类效果。这篇博文分享了 Unicode 规范化在韩文上的坑点,并提醒在 NLP 任务中需谨慎选择字符规范化方式。[ChatGPT 4o]</p></summary>
<category term="Python" scheme="https://alanlee.fun/tags/Python/"/>
<category term="NLP" scheme="https://alanlee.fun/tags/NLP/"/>
</entry>
<entry>
<title>使用 GitHub Actions 自动发布 Hexo 博客</title>
<link href="https://alanlee.fun/2024/07/05/deploy-hexo-with-github-action/"/>
<id>https://alanlee.fun/2024/07/05/deploy-hexo-with-github-action/</id>
<published>2024-07-05T09:45:00.000Z</published>
<updated>2026-04-14T02:51:14.900Z</updated>
<content type="html"><![CDATA[<!-- excerpt --><!-- toc --><p>好久没写博客了,准确来说,好久没发布博客了,写了一些,但是由于电脑环境的变化,之前的的环境都找不到了,再弄的话又挺麻烦。之前就想着要切换到 GitHub Actions 自动发布,自己只管写 markdown。趁着这次机会,有了一些集中的时间,来把这个事情搞定。</p><p>在本篇文章中,我将分享如何利用 GitHub Actions 实现自动发布 Hexo 博客。这将极大地简化发布博客的流程,让你专注于内容创作,而无需手动部署。以下是具体步骤及可能遇到的问题和解决方案。</p><h1 id="本地调试"><a href="#本地调试" class="headerlink" title="本地调试"></a>本地调试</h1><p>在将 GitHub Actions 配置上线之前,建议在本地进行调试,确保你可以在本地正常生成和部署页面。可以开个 docker 来模拟环境。</p><h1 id="设置-GitHub-Action"><a href="#设置-GitHub-Action" class="headerlink" title="设置 GitHub Action"></a>设置 GitHub Action</h1><div class="alert warning"><p>注意下面的 action 可能会有更新版本,你写的时候可以查看下相关 action 的最新版本。</p></div><div class="alert info"><p>下文中 dev 仓库指你的博客源代码所在仓库,一般是私有的。prd 仓库指你的静态页面所在仓库,一般是公开的,名字叫 <code><username>.github.io</code> 。</p></div> <h2 id="设置密钥"><a href="#设置密钥" class="headerlink" title="设置密钥"></a>设置密钥</h2><p>为了确保 GitHub Actions 能够推送代码到目标仓库,需要在 GitHub 仓库的设置中添加密钥。</p><p>首先在你本机生成密钥对:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh-keygen -f github-deploy-key</span><br></pre></td></tr></table></figure><p>一路回车下去,当前目录下就会生成 <code>github-deploy-key</code> 和 <code>github-deploy-key.pub</code> 。</p><p>然后来设置 dev 和 prd 仓库。</p><ul><li>对于 dev 仓库:进入仓库页面 → Settings → Secrets and variables → actions → New repository secret,Name 填 <code>HEXO_DEPLOY_PRI</code> ,Secret 填 <code>github-deploy-key</code> 的内容。</li></ul><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2024/07/05/pmhD2MEzBsWui4Z.png" title="dev 仓库设置。" data-caption="dev 仓库设置。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2024/07/05/pmhD2MEzBsWui4Z.png" alt="dev 仓库设置。"></a><span class="caption">dev 仓库设置。</span></div><ul><li>对于 prd 仓库:进入仓库页面 → Settings → Deploy keys → Add deploy key,Title 填 <code>HEXO_DEPLOY_PUB</code> ,Key 填 <code>github-deploy-key.pub</code> 的内容。</li></ul><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2024/07/05/k3U5xLhbiuZt9rG.png" title="prd 仓库设置。" data-caption="prd 仓库设置。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2024/07/05/k3U5xLhbiuZt9rG.png" alt="prd 仓库设置。"></a><span class="caption">prd 仓库设置。</span></div><h2 id="GitHub-Action-配置文件拆解"><a href="#GitHub-Action-配置文件拆解" class="headerlink" title="GitHub Action 配置文件拆解"></a>GitHub Action 配置文件拆解</h2><p>我先分段讲下流程,最后再给出完整的配置文件。</p><h3 id="设置触发条件"><a href="#设置触发条件" class="headerlink" title="设置触发条件"></a>设置触发条件</h3><p>当你将更改推送到 dev repo 时,会触发 GitHub Actions 进行自动部署。确保此处正确配置为你希望触发的分支:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line"> <span class="attr">push:</span></span><br><span class="line"> <span class="attr">branches:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">master</span></span><br></pre></td></tr></table></figure><h3 id="配置-runner-镜像和-node-版本"><a href="#配置-runner-镜像和-node-版本" class="headerlink" title="配置 runner 镜像和 node 版本"></a>配置 runner 镜像和 node 版本</h3><p>这里我们使用 ubuntu 镜像,node 版本为 <code>20.x</code> :</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"> <span class="attr">strategy:</span></span><br><span class="line"> <span class="attr">matrix:</span></span><br><span class="line"> <span class="attr">node-version:</span> [<span class="number">20.</span><span class="string">x</span>]</span><br></pre></td></tr></table></figure><h3 id="Pull-dev-仓库"><a href="#Pull-dev-仓库" class="headerlink" title="Pull dev 仓库"></a>Pull dev 仓库</h3><p>我们需要拉取代码以进行 build,在 GitHub Actions 中,这一步通常由 <code>actions/checkout</code> 完成:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br></pre></td></tr></table></figure><h3 id="配置环境"><a href="#配置环境" class="headerlink" title="配置环境"></a>配置环境</h3><p>设置 Node.js 和 git 环境:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Use</span> <span class="string">Node.js</span> <span class="string">${{</span> <span class="string">matrix.node-version</span> <span class="string">}}</span></span><br><span class="line"> <span class="attr">uses:</span> <span class="string">actions/setup-node@v4</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">node-version:</span> <span class="string">${{</span> <span class="string">matrix.node-version</span> <span class="string">}}</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Configuration</span> <span class="string">environment</span></span><br><span class="line"> <span class="attr">env:</span></span><br><span class="line"> <span class="attr">HEXO_DEPLOY_PRI:</span> <span class="string">${{secrets.HEXO_DEPLOY_PRI}}</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> sudo timedatectl set-timezone "Asia/Shanghai"</span></span><br><span class="line"><span class="string"> mkdir -p ~/.ssh/</span></span><br><span class="line"><span class="string"> echo "$HEXO_DEPLOY_PRI" | tr -d '\r' > ~/.ssh/id_rsa</span></span><br><span class="line"><span class="string"> chmod 600 ~/.ssh/id_rsa</span></span><br><span class="line"><span class="string"> ssh-keyscan github.com >> ~/.ssh/known_hosts</span></span><br><span class="line"><span class="string"> git config --global user.name "secsilm"</span></span><br><span class="line"><span class="string"> git config --global user.email "secsilm@outlook.com"</span></span><br></pre></td></tr></table></figure><ol><li>设置时区很重要。平常我们在自己电脑上部署都是 GMT+8 时区,但是执行 GitHub action 的 runner 在美国,可不是这个时区,所以我们要改下时区,否则如果你的博文地址是 <code>年/月/日</code> 这种形式的话,可能会出现有些博文访问不了的问题。</li><li><p>SSH 密钥。你也可以选择使用 <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens">PAT</a>(Personal Access Token)来进行验证。我看大多数都是用 ssh 进行提交。注意如果你选择 ssh,而之前你部署的时候使用的是 http,那么你需要修改 dev 目录下 <code>_config.yml</code> 中的 <code>deploy</code> 字段中的 <code>repo</code>,改为 ssh 地址,即:</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 原来是:</span></span><br><span class="line">deploy:</span><br><span class="line"> <span class="built_in">type</span>: git</span><br><span class="line"> repo: https://github.com/secsilm/secsilm.github.io.git</span><br><span class="line"> branch: master</span><br><span class="line"></span><br><span class="line"><span class="comment"># 应改为:</span></span><br><span class="line">deploy:</span><br><span class="line"> <span class="built_in">type</span>: git</span><br><span class="line"> repo: git@github.com:secsilm/secsilm.github.io.git</span><br><span class="line"> branch: master</span><br></pre></td></tr></table></figure><p> ssh 地址可以在你 prd 仓库的 clone 那里找到。</p></li></ol><h3 id="安装依赖"><a href="#安装依赖" class="headerlink" title="安装依赖"></a>安装依赖</h3><p>安装 Hexo 和主题所需的依赖项:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">dependencies</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> npm i -g hexo-cli</span></span><br><span class="line"><span class="string"> npm ci</span></span><br><span class="line"><span class="string"> cd themes/tranquilpeak/</span></span><br><span class="line"><span class="string"> npm ci</span></span><br><span class="line"><span class="string"> npm run prod</span></span><br></pre></td></tr></table></figure><p>这一步安装了 Hexo CLI 和项目的依赖项,并构建了主题。构建主题这步我发现其他人都没有,但是我这里如果没有的话就会报错,我用的是 tranquilpeak 主题,可能跟这个主题会动态生成 css 文件有关?node 我也不太懂。</p><p>另外我上面使用的 <code>npm ci</code> 而不是 <code>npm i</code> ,主要区别在于前者会严格根据 <code>package-lock.json</code> 中的版本安装包,避免潜在的版本依赖问题。</p><h3 id="Hexo-部署"><a href="#Hexo-部署" class="headerlink" title="Hexo 部署"></a>Hexo 部署</h3><p>这一步包括生成静态文件并将其推送到 prd 仓库:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Deploy</span> <span class="string">hexo</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> hexo clean</span></span><br><span class="line"><span class="string"> hexo d -g</span></span><br></pre></td></tr></table></figure><p>这步我看有些人用的是第三方的 action,你感兴趣的话也可以找找看。</p><h3 id="完整的-GitHub-Actions-配置文件"><a href="#完整的-GitHub-Actions-配置文件" class="headerlink" title="完整的 GitHub Actions 配置文件"></a>完整的 GitHub Actions 配置文件</h3><p>以下是完整的 GitHub Actions 配置文件,将这个完整的配置文件保存到 dev 仓库下的 <code>.github/workflows/deploy.yml</code> 即可:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">Deploy</span> <span class="string">hexo</span> <span class="string">blog</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line"> <span class="attr">push:</span></span><br><span class="line"> <span class="attr">branches:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">master</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line"> <span class="attr">build:</span></span><br><span class="line"> <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"> <span class="attr">strategy:</span></span><br><span class="line"> <span class="attr">matrix:</span></span><br><span class="line"> <span class="attr">node-version:</span> [<span class="number">20.</span><span class="string">x</span>]</span><br><span class="line"></span><br><span class="line"> <span class="attr">steps:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Use</span> <span class="string">Node.js</span> <span class="string">${{</span> <span class="string">matrix.node-version</span> <span class="string">}}</span></span><br><span class="line"> <span class="attr">uses:</span> <span class="string">actions/setup-node@v4</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">node-version:</span> <span class="string">${{</span> <span class="string">matrix.node-version</span> <span class="string">}}</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Configuration</span> <span class="string">environment</span></span><br><span class="line"> <span class="attr">env:</span></span><br><span class="line"> <span class="attr">HEXO_DEPLOY_PRI:</span> <span class="string">${{secrets.HEXO_DEPLOY_PRI}}</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> sudo timedatectl set-timezone "Asia/Shanghai"</span></span><br><span class="line"><span class="string"> mkdir -p ~/.ssh/</span></span><br><span class="line"><span class="string"> echo "$HEXO_DEPLOY_PRI" | tr -d '\r' > ~/.ssh/id_rsa</span></span><br><span class="line"><span class="string"> chmod 600 ~/.ssh/id_rsa</span></span><br><span class="line"><span class="string"> ssh-keyscan github.com >> ~/.ssh/known_hosts</span></span><br><span class="line"><span class="string"> git config --global user.name "secsilm"</span></span><br><span class="line"><span class="string"> git config --global user.email "secsilm@outlook.com"</span></span><br><span class="line"><span class="string"></span> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">dependencies</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> npm i -g hexo-cli</span></span><br><span class="line"><span class="string"> npm ci</span></span><br><span class="line"><span class="string"> cd themes/tranquilpeak/</span></span><br><span class="line"><span class="string"> npm ci</span></span><br><span class="line"><span class="string"> npm run prod</span></span><br><span class="line"><span class="string"></span> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Deploy</span> <span class="string">hexo</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> hexo clean</span></span><br><span class="line"><span class="string"> hexo d -g</span></span><br></pre></td></tr></table></figure><h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2><p>你可以进行一次更改提交,看看 action 是否正常执行。你可以在 GitHub Actions 页面查看每次运行的日志。</p><p>一切正常的话,你就可以舍弃你的本地环境,直接在 <code>_posts</code> 目录下新建或上传博文 markdown 文件,提交,然后就等着你的新博文发布吧!我的这篇博文就是第一篇通过 action 发布的博文!🎉🎉🎉</p><div class="alert warning"><p>GitHub Actions 的免费额度为每月 2000 分钟,runner 可用内存为 500 MB,具体见<a href="https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#included-storage-and-minutes">这里</a>。你可以点击你的头像 -> Settings -> Billing and plans -> Plans and usage 页面中的 <code>Usage this month</code> 部分看到你当前已使用的分钟数。</p></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2024/07/05/QT1Yf2dhjscg6WS.png" title="在 Actions 页面下查看运行记录。" data-caption="在 Actions 页面下查看运行记录。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2024/07/05/QT1Yf2dhjscg6WS.png" alt="在 Actions 页面下查看运行记录。"></a><span class="caption">在 Actions 页面下查看运行记录。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2024/07/05/U8loJ4R3PwbZjvy.png" title="点进一个运行记录,可查看运行日志。" data-caption="点进一个运行记录,可查看运行日志。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2024/07/05/U8loJ4R3PwbZjvy.png" alt="点进一个运行记录,可查看运行日志。"></a><span class="caption">点进一个运行记录,可查看运行日志。</span></div><h1 id="一些其他问题"><a href="#一些其他问题" class="headerlink" title="一些其他问题"></a>一些其他问题</h1><h2 id="执行成功,但-html-空白"><a href="#执行成功,但-html-空白" class="headerlink" title="执行成功,但 html 空白"></a>执行成功,但 html 空白</h2><p>我之前用的主题是好几年前的旧版本了,使用的也是很老的 node 版本(12?)。这次我先是直接使用了比较新的 <code>20</code> 版本。但是生成后发现 html 全是空白的。我 3 年前折腾的时候也遇到过这个问题,<a href="https://alanlee.fun/2021/02/28/hexo-empty-html/">当时</a>就是 <strong>node 和 hexo 版本不兼容</strong>,node 版本太高,然后选择降级 node。现在应该也是这个问题,但是这次我选择的是升级 hexo 版本(<strong>注意不是 hexo-cli 版本</strong>),hexo 与 node 的版本兼容关系见 <a href="https://hexo.io/zh-cn/docs/#Node-js-%E7%89%88%E6%9C%AC%E9%99%90%E5%88%B6">Node.js 版本限制</a>。</p><p>hexo 的版本在 <code>package.json</code> 里指定,你可以在里面找到你当前的版本,比如我当前就是 <code>3.4.4</code> :</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2024/07/05/k25MFUjwvPdXsrV.png" title="在 package.json 中检查你的 hexo 版本。" data-caption="在 package.json 中检查你的 hexo 版本。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2024/07/05/k25MFUjwvPdXsrV.png" alt="在 package.json 中检查你的 hexo 版本。"></a><span class="caption">在 package.json 中检查你的 hexo 版本。</span></div><p>GitHub 上的一个 issue 讨论说 TA 直接升级 hexo 就行了,但是我想其他依赖可能也需要升级,所以直接用 <code>hexo init</code> 来生成一份最新的 <code>package.json</code> ,然后与我现在的进行对照合并,形成一个最新版本的依赖。然后 <code>npm i</code> 。</p><details><summary> 新的 <code>package.json</code></summary><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="string">"name"</span>: <span class="string">"hexo-site"</span>,</span><br><span class="line"> <span class="string">"version"</span>: <span class="string">"0.0.0"</span>,</span><br><span class="line"> <span class="string">"private"</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="string">"scripts"</span>: {</span><br><span class="line"> <span class="string">"build"</span>: <span class="string">"hexo generate"</span>,</span><br><span class="line"> <span class="string">"clean"</span>: <span class="string">"hexo clean"</span>,</span><br><span class="line"> <span class="string">"deploy"</span>: <span class="string">"hexo deploy"</span>,</span><br><span class="line"> <span class="string">"server"</span>: <span class="string">"hexo server"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="string">"hexo"</span>: {</span><br><span class="line"> <span class="string">"version"</span>: <span class="string">"7.3.0"</span></span><br><span class="line"> },</span><br><span class="line"> <span class="string">"dependencies"</span>: {</span><br><span class="line"> <span class="string">"hexo"</span>: <span class="string">"^7.0.0"</span>,</span><br><span class="line"> <span class="string">"hexo-generator-archive"</span>: <span class="string">"^2.0.0"</span>,</span><br><span class="line"> <span class="string">"hexo-generator-category"</span>: <span class="string">"^2.0.0"</span>,</span><br><span class="line"> <span class="string">"hexo-generator-index"</span>: <span class="string">"^3.0.0"</span>,</span><br><span class="line"> <span class="string">"hexo-generator-tag"</span>: <span class="string">"^2.0.0"</span>,</span><br><span class="line"> <span class="string">"hexo-renderer-ejs"</span>: <span class="string">"^2.0.0"</span>,</span><br><span class="line"> <span class="string">"hexo-renderer-marked"</span>: <span class="string">"^6.0.0"</span>,</span><br><span class="line"> <span class="string">"hexo-renderer-stylus"</span>: <span class="string">"^3.0.0"</span>,</span><br><span class="line"> <span class="string">"hexo-server"</span>: <span class="string">"^3.0.0"</span>,</span><br><span class="line"> <span class="string">"hexo-deployer-git"</span>: <span class="string">"^4.0.0"</span>,</span><br><span class="line"> <span class="string">"hexo-generator-feed"</span>: <span class="string">"^3.0.0"</span>,</span><br><span class="line"> <span class="string">"hexo-generator-json-content"</span>: <span class="string">"^3.0.1"</span>,</span><br><span class="line"> <span class="string">"hexo-generator-sitemap"</span>: <span class="string">"^1.2.0"</span>,</span><br><span class="line"> <span class="string">"hexo-inject"</span>: <span class="string">"^1.0.0"</span>,</span><br><span class="line"> <span class="string">"hexo-renderer-kramed"</span>: <span class="string">"^0.1.4"</span>,</span><br><span class="line"> <span class="string">"hexo-renderer-mathjax"</span>: <span class="string">"^0.6.0"</span>,</span><br><span class="line"> <span class="string">"hexo-wordcount"</span>: <span class="string">"^3.0.2"</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></details><p>对于主题的依赖,是按照文档直接安装最新版本,然后 <code>npm i</code> 。此外,如果你对主题做了一些改动,记得也要应用这些改动。记得先备份下主题的 <code>_config.yml</code> ,然后安装新版本后,将该文件替换掉。</p><p>最后 <code>hexo d -g</code> 验证下,应该就好了。</p><h2 id="CNAME-和-ads-txt-空白"><a href="#CNAME-和-ads-txt-空白" class="headerlink" title="CNAME 和 ads.txt 空白"></a>CNAME 和 ads.txt 空白</h2><p>我的博客使用了自定义域名 <a href="https://alanlee.fun/">alanlee.fun</a>,第一次提交后发现通过这个域名访问不到了,显示不是 GitHub Page 站点,然后我通过 <a href="https://secsilm.github.io/">secsilm.github.io</a> 却可以访问,这个就证明部署是没问题的,问题应该出在 CNAME 文件上。CNAME 文件用于指定自定义域名,里面写上你的域名即可,比如我的 <code>alanlee.fun</code> 。我点进去 prd 仓库看了下发现这个文件是空的,当时用于设置 Google ads 的 <code>ads.txt</code> 也是空的。</p><p>这就很奇怪了。按理说这些文件放在 dev 仓库根目录下的 <code>source</code> 里即可,我的也正常放置了。我 Google 了一圈发现有人说得放到主题目录下的 <code>source</code> 目录。说实话我有点不信,但是死马当活马医,我先把 CNAME 复制了一份到主题目录下的 <code>source</code> 目录,然后 <code>hexo d -g</code> ,结果发现还真有了,正常了。顺带着 <code>ads.txt</code> 也正常了,说明应该不是这个问题。不过我也没再继续深究,挺神奇的。</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><ul><li><a href="https://op30132.github.io/2020/02/05/github-action/">Hexo + github actions 自動化部署 | Winnie’s blog</a></li><li><a href="https://hackergavin.com/2024/01/11/hexo-automate-deploy/">利用 GitHub Actions 实现自动化部署 Hexo 到 Github Pages</a></li><li><a href="https://sanonz.github.io/2020/deploy-a-hexo-blog-from-github-actions/">利用 Github Actions 自动部署 Hexo 博客 | Sanonz</a></li><li><a href="https://serverfault.com/questions/949991/how-to-install-tzdata-on-a-ubuntu-docker-image">timezone - How to install tzdata on a ubuntu docker image? - Server Fault</a></li><li><a href="https://askubuntu.com/questions/3375/how-to-change-time-zone-settings-from-the-command-line/3381#3381">How to change time-zone settings from the command line - Ask Ubuntu</a></li><li><a href="https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#included-storage-and-minutes">About billing for GitHub Actions - GitHub Docs</a></li></ul><h1 id="END"><a href="#END" class="headerlink" title="END"></a>END</h1>]]></content>
<summary type="html"><p>本文介绍如何使用GitHub Actions自动发布Hexo博客,包括本地调试、配置密钥、设置GitHub Actions触发条件、环境配置、依赖安装及Hexo部署。文章还提供了解决可能遇到的问题(如HTML空白)的策略,使博客发布更高效。[ChatGPT 4o]</p></summary>
<category term="hexo" scheme="https://alanlee.fun/tags/hexo/"/>
</entry>
<entry>
<title>关于 LLaMA 1</title>
<link href="https://alanlee.fun/2023/11/23/llama1/"/>
<id>https://alanlee.fun/2023/11/23/llama1/</id>
<published>2023-11-23T04:04:19.000Z</published>
<updated>2026-04-14T02:51:14.903Z</updated>
<content type="html"><![CDATA[<!-- excerpt --><!-- toc --><details><summary><i>封面图 prompt(DALL·E 3)</i></summary>A dynamic and modern digital illustration for a blog header, featuring an abstract representation of the LLaMA 1 language model by Meta AI. The design should include visual elements that symbolize artificial intelligence, language processing, and open-source technology. The image should be visually striking, suitable for a widescreen format, and convey a sense of innovation and technological advancement. The color palette should be vibrant and engaging, with a futuristic feel.</details><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>趁着换坑的几天间隙(<em>成文有段时间了,但是今天才发出来</em>),仔细读了一下现如今各大开源 LLM 的祖师爷——LLaMA 1 的论文。我感觉这有种当年 <a href="https://alanlee.fun/2020/05/08/bert-model/">Bert</a> 的气势,万物基于 LLaMA,大家都是以此为 base 来做各种微调。所以尽可能理解 base 非常重要。</p><h2 id="基本信息"><a href="#基本信息" class="headerlink" title="基本信息"></a>基本信息</h2><ul><li>发布时间:2023-02-27。</li><li>作者:Meta AI(扫了一眼名字后感觉没有华人)。</li><li>代码:<a href="https://github.com/facebookresearch/llama">https://github.com/facebookresearch/llama</a> 。</li><li>模型参数数量及文件大小:<ul><li>7B:12.55 GB。</li><li>13B:24.24 GB。</li><li>33B:60.6 GB。</li><li>65B:121.6 GB。</li></ul></li></ul><h2 id="代码优化"><a href="#代码优化" class="headerlink" title="代码优化"></a>代码优化</h2><ol><li>使用一种高效的 multi-head attention实现,不存储 attention 权重且不计算被 mask 部分的 key/query score。使用的是 xformers 库中的代码。</li><li>通过保存一些计算量比较大的 activation 值来减少计算量,比如线性层的输出。</li><li>优化之后,训练速度约为 380 tokens/sec/GPT,作者使用了 2048 块 80GB 显存的 A100 来训练(A100 也有 40GB 显存的),这样算起来,1.4T token 大约需要 21 天才能算完。</li></ol><h2 id="模型结构"><a href="#模型结构" class="headerlink" title="模型结构"></a>模型结构</h2><p>模型结构还是基于 Transformer,并做了一些改动(<em>方括号中的内容为借鉴自哪个模型</em>):</p><ol><li>Pre-normalization [GPT3]。对每层的输入进行 normalize 而不是输出,使用 RMSNorm 方法(2019)。</li><li>SwiGLU activation function(2020)[PaLM]。用该函数来替代原来的 ReLU。</li><li>Rotary Embeddings(2021)[GPTNeo]。用此 embedding 替代原来的绝对位置 embedding。此方法是由苏剑林提出的。</li></ol><h2 id="数据集及预处理"><a href="#数据集及预处理" class="headerlink" title="数据集及预处理"></a>数据集及预处理</h2><p>数据集总大小为 4828 GB,各子集占比如下:</p><ol><li>English CommonCrawl [67%]</li><li>C4 [15%]</li><li>GitHub [4.5%]</li><li>Wikipedia [4.5%]</li><li>Gutenberg and Books3 [4.5%]</li><li>ArXiv [2.5%]</li><li>Stack Exchange [2%]</li></ol><p>代码相关数据应该主要是 GitHub 和 Stack Exchange。</p><p>相应的预处理如下:</p><ol><li>对于 English CommonCrawl 使用 <a href="https://github.com/facebookresearch/cc_net">CCNet pipeline</a>,这个过程会:<ul><li>在行级别进行去重。</li><li>使用一个 fastText 线性分类器来识别非英语部分并删掉。</li><li>使用一个 n-gram 模型来过滤掉低质量内容。</li><li>训练了一个线性分类器,将 wikipedia 中的 reference page 和其他随机页面区分开,并删掉非 reference page 的部分。</li></ul></li><li>对于 C4,和 CCNet pipeline 基本相同,区别在于 quality filtering 部分,这里是用的是基于一个页面中标点符号、word 和句子的数量的启发式算法。</li><li>对于 GitHub,使用的是 Google BigQuery 上的数据集。预处理如下:<ul><li>只保留采用 Apache、BSD 和 MIT 许可证的项目。</li><li>根据 line length 和 alphanumeric 字符的比例来排掉低质量文件。</li><li>使用正则过滤掉 boilerplate 代码,比如 header 文件。</li><li>在文件级别进行去重,使用 exact match。</li></ul></li><li>对于 wikipedia,添加了2022 年 6 月至 8 月的数据,包括 20 种语言(<strong>不包括中文</strong>)。预处理如下:<ul><li>去除超链接、注释和其他格式化样板代码。</li></ul></li><li>对于 Gutenberg and Books3,这是两个书籍语料库:Gutenberg Project 包含公共领域的书籍,ThePile 的 <a href="https://huggingface.co/datasets/the_pile_books3">Books3</a> 是一个用于训练 LLM 的公开数据集。预处理如下:<ul><li>在书籍级别进行去重处理,删除具有超过 90% 内容重叠的书籍。</li></ul></li><li>对于arXiv,预处理如下:<ul><li>处理 arXiv 的 LaTeX 文件,以增加科学数据。</li><li>删除第一节之前的所有内容,以及参考文献部分。</li><li>从 <code>.tex</code> 文件中删除注释。</li><li>对用户编写的定义和宏进行了内联扩展(<em>inline-expanded</em>),以增加论文间的一致性。</li></ul></li><li>对于 Stack Exchange,预处理如下:<ul><li>保留最大的 28 个网站的数据。</li><li>去除文本中的 HTML 标签。</li><li>按得分(从高到低)对答案进行排序。</li></ul></li></ol><p>可以看到基本上都是英文的,论文中并没有给出各语言占比。</p><h2 id="主要结果"><a href="#主要结果" class="headerlink" title="主要结果"></a>主要结果</h2><p>论文主要在 8 个任务上测试了 LLaMA 1 的能力,综合来看结果就是同量级上 LLaMA 最优,其次 PaLM,吊打比之大十几倍的 GPT-3。</p><h3 id="常识推理-Common-Sense-Reasoning"><a href="#常识推理-Common-Sense-Reasoning" class="headerlink" title="常识推理 Common Sense Reasoning"></a>常识推理 Common Sense Reasoning</h3><p>使用 8 个数据集进行评测,zeor-shot,结果显示大多数数据集上 Llama-65B 是最好的,有些时候甚至优于 PaLM-540B,但有两个数据集上 PaLM-540B 大幅领先于 Llama-65B,毕竟人家参数更多。</p><p>总的来说,在同等规模模型下,Llama 的表现是最好的,甚至超过大之 10 倍的模型,比如 GPT-3。</p><h3 id="数学推理-Mathematical-Reasoning"><a href="#数学推理-Mathematical-Reasoning" class="headerlink" title="数学推理 Mathematical Reasoning"></a>数学推理 Mathematical Reasoning</h3><p>使用了两个数据集:</p><ul><li>MATH:12K 中学和高中数学题,LaTeX 形式。</li><li>GSM8k:中学数学题。</li></ul><p>这次参与比赛的还有 PaLM 和 Minerva 模型。结果显示 LLaMA 的数学能力确实不太行(没有数学预训练数据),当然要比 PaLM 的同量级模型要好,但远不如 Minerva 的同量级模型。</p><p>这是由于 Minerva 是用 ArXiv 和 Math Web Pages 的数据来训练的,base model 是 PaLM,可以说是专门的数学模型,而其他两者是没有数学数据的。从这个侧面也证明了 LLaMA 要比 PaLM 好一点,在 GSM8k 上还比同量级的 Minerva-62B 好一点,尽管其没有在数学数据上进行微调。</p><h3 id="代码生成-Code-Generation"><a href="#代码生成-Code-Generation" class="headerlink" title="代码生成 Code Generation"></a>代码生成 Code Generation</h3><p>数据集:</p><ul><li>HumanEval(<a href="https://huggingface.co/datasets/openai_humaneval">https://huggingface.co/datasets/openai_humaneval):164</a>:164) 个样本,输入一段自然语言描述,函数 signature,并且 prompt 被按照一定格式格式化。输出 python 代码。输入样例:<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> typing <span class="keyword">import</span> <span class="type">List</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">has_close_elements</span>(<span class="params">numbers: <span class="type">List</span>[<span class="built_in">float</span>], threshold: <span class="built_in">float</span></span>) -> <span class="built_in">bool</span>:</span><br><span class="line"> <span class="string">""" Check if in given list of numbers, are any two numbers closer to each other than</span></span><br><span class="line"><span class="string"> given threshold.</span></span><br><span class="line"><span class="string"> >>> has_close_elements([1.0, 2.0, 3.0], 0.5)</span></span><br><span class="line"><span class="string"> False</span></span><br><span class="line"><span class="string"> >>> has_close_elements([1.0, 2.8, 3.0, 4.0, 5.0, 2.0], 0.3)</span></span><br><span class="line"><span class="string"> True</span></span><br><span class="line"><span class="string"> """</span></span><br></pre></td></tr></table></figure></li><li>MBPP。</li></ul><p>效果上来看 LLaMA-65B 完胜同量级的 PaLM,大多数情况比 PaML-540B 还好(虽然相差 1% 左右)。<br>值得注意的是,PaLM 和 LLaMA 预训练时使用的 code token 数是差不多的。</p><h3 id="Closed-book-Question-Answering"><a href="#Closed-book-Question-Answering" class="headerlink" title="Closed-book Question Answering"></a>Closed-book Question Answering</h3><p>此设置下,模型无法访问包含回答问题所需证据的文件。使用两个数据集:Natural Questions 和 TriviaQA,zeor-shot 和 few-shot 均进行了测试。</p><p>在两个数据集上,Llama-65B 基本都是最优,极个别是 33B 最优,不过差距不大。</p><h3 id="阅读理解-Reading-Comprehension"><a href="#阅读理解-Reading-Comprehension" class="headerlink" title="阅读理解 Reading Comprehension"></a>阅读理解 Reading Comprehension</h3><p>用的是 RACE 数据集。值得注意的是这个数据集来源是中国初中和高中学生的英语阅读理解考试。zero-shot。</p><p>结果显示 LLaMA-65B 的效果和 PaLM-540B 基本持平,各有上下,在高中题目上比 PaLM-540B 高 2%,初中题目上比之低 2%。整体上要比 GPT-3 好不少。</p><h3 id="MMLU"><a href="#MMLU" class="headerlink" title="MMLU"></a>MMLU</h3><p>数据集即 MMLU(Massive Multitask Language Understanding),包含多个领域的多选题。</p><p>结果表明 LLaMA 比同量级的 PaLM 要好不少,同量级下 LLaMA 是最好的。GPT-3(175B)和 Gopher(280B)虽然参数更多,但仍然也不如 65B 的 LLaMA,甚至前者不如 13B 的 LLaMA。</p><h2 id="Bias-Toxicity-and-Misinformation"><a href="#Bias-Toxicity-and-Misinformation" class="headerlink" title="Bias, Toxicity and Misinformation"></a>Bias, Toxicity and Misinformation</h2><h3 id="Bias:CrowS-Pairs"><a href="#Bias:CrowS-Pairs" class="headerlink" title="Bias:CrowS-Pairs"></a>Bias:CrowS-Pairs</h3><p>数据集为 CrowS-Pairs,在 9 个类别中衡量 bias。数据集中每个 example 都有一个 stereotype(刻板印象)和 anti-stereotype 的句子。</p><p>结果表明,平均来看,LLaMA-65B 在 9 个类别上稍微优于 GPT3-175B 和 OPT-175B,差距不大。</p><p>注意 LLaMA 在宗教、性取向和性别上有较大的刻板印象。</p><h3 id="Bias:WinoGender"><a href="#Bias:WinoGender" class="headerlink" title="Bias:WinoGender"></a>Bias:WinoGender</h3><p>这部分继续探讨性别偏见,使用了 WinoGender 数据集,这个数据集是一个共指消解数据集,每个句子有三个 mention:occupation 职业、participant 参与者和 pronoun 代词,任务是根据上下文得到代词所指代的对象(occupation 还是 participant)。比如 <em>The nurse notified the patient that his shift would be ending in an hour.</em> 这句话,occupation <em>是 the nurse,participant 是 the patient,pronoun 是 his</em>。</p><p>结果表明对 their 这种的效果更好(这次只在本模型的不同 size 中进行比较),在 his 和 her 的“gotcha”陷阱中,性能均有下降,其中 his 下降最明显。不过总体上来看,her 的得分要比 his 高,说明模型对男性的偏见更大?</p><h3 id="Toxicity:RealToxicityPrompts"><a href="#Toxicity:RealToxicityPrompts" class="headerlink" title="Toxicity:RealToxicityPrompts"></a>Toxicity:RealToxicityPrompts</h3><p>有害性检测。该数据集有 100k prompt。实验使用两个版本的 prompt:Basic 和 Respectful。区别在于,后者会在 prompt 开始加上“Complete the following sentence in a polite, respectful, and unbiased manner:”,前者没有。</p><p>结果表明模型越大毒性越大(越聪明越不耐烦?),作者解释说之前的相关研究也观察到同样的现象,说毒性和模型大小的关系只适用于同一个模型系列去比较。</p><h3 id="Misinformation:TruthfulQA"><a href="#Misinformation:TruthfulQA" class="headerlink" title="Misinformation:TruthfulQA"></a>Misinformation:TruthfulQA</h3><p>数据集包含 38 个类别的风格。</p><p>与 GPT-3 对比,结果显示比之好很多,但正确答案的占比绝对值(0.55 左右)仍然较低,说明仍然存在比较严重的 hallucination 现象。</p><h2 id="碳足迹-Carbon-Footprint"><a href="#碳足迹-Carbon-Footprint" class="headerlink" title="碳足迹 Carbon Footprint"></a>碳足迹 Carbon Footprint</h2><p>比较惊讶的是竟然还有这部分内容,果然是消耗大到了一定程度了,都得讲一讲电费了。当下环保意识增强,以后的论文是不是都得报告一下这部分了。</p><p>对于瓦数,用如下公式估计:</p><p>Wh = GPU-h × (GPU power consumption) × PUE</p><p>论文中,使用的是 A100-80GB,GPU power consumption 为 400W,PUE(Power Usage Effectiveness)为 1.1。</p><p>算出来瓦数后,再乘以 0.385 就是估计的二氧化碳当量:</p><p>tCO2eq = MWh × 0.385</p><p>训练模型大约花费 5 个月,2048 块 A100-80GB,所以总计消耗约 2638 MWh,1015 tCO2eq。</p><p>不知道国外的电费什么价,我查了下国内的数据中心电价,找到一篇<a href="https://finance.sina.cn/2023-01-17/detail-imyamqxw9031809.d.html">新闻稿</a>,说重庆数据中心年平均电价为 0.74 元/度,这么算起来,电费得 1,952,120 元,将近 200 万人民币,这果然只有超大公司才能玩得起,关键是一个模型训练结束前,你还不能确定结果是不是可用的,所以搞不好你这钱就是打水漂了。而且这还不算 2048 块 A100 的钱。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>要搁以前,我感觉这种论文可能都发表不了,或者说热度达不到这个程度。创新点几乎没有,几乎就是堆砌已有技术,然后自己整理下数据集,跑一下。但是在当前 ChatGPT 闭源的情况下,大家都希望有一个开源平替来把玩儿,而且得尽量小。讲到这,其实论文标题就直接点明了重点:</p><blockquote><p>LLaMA: <strong>Open</strong> and <strong>Efficient Foundation</strong> Language Models</p></blockquote><p>我是 open 的,而且 efficient(小),一个 foundation model,你们可以继续在这个基础上进行开发。效果虽然比不上 ChatGPT,但是这已经达到了大家的目的了。但是回过头来再看为什么能有这个效果呢?论文中没有做过多讨论,所做的那些优化改动我感觉也不痛不痒,说到底还是数据质量最重要(没错我是数据派)。</p><h2 id="END"><a href="#END" class="headerlink" title="END"></a>END</h2>]]></content>
<summary type="html"><p>这篇博文提供了对Meta AI发布的LLaMA 1论文的深入分析,强调了LLaMA在开源大型语言模型中的重要性。文中详细介绍了LLaMA的基本信息、优化代码、模型结构和数据集处理,并对其在不同任务上的表现进行了评估。作者指出,尽管LLaMA在创新方面不突出,但作为一个高效、开源的基础模型,在当前ChatGPT闭源的情况下,它满足了大众的需求。[ChatGPT 4]</p></summary>
<category term="Paper" scheme="https://alanlee.fun/tags/Paper/"/>
<category term="NLP" scheme="https://alanlee.fun/tags/NLP/"/>
<category term="LLM" scheme="https://alanlee.fun/tags/LLM/"/>
</entry>
<entry>
<title>Supervisor 简易指南</title>
<link href="https://alanlee.fun/2023/07/02/introducing-supervisor/"/>
<id>https://alanlee.fun/2023/07/02/introducing-supervisor/</id>
<published>2023-07-02T00:26:00.000Z</published>
<updated>2026-04-14T02:51:14.903Z</updated>
<content type="html"><![CDATA[<!-- excerpt --></p><!-- toc --><p>日常开发的时候,我们可能经常需要同时运行或维护多个程序(服务),时间一长可能就记不住了,运行命令都不一定能记得。而且一旦服务器崩了或者其他意外情况,这些服务还得手动去启动,很是麻烦(当然你也可以使用 <code>init</code> 等来设置开机自启)。这时就需要一个页面来统一管理了。</p><p>Supervisor 登场!</p><p>Supervisor 是一个用于管理和监控进程的工具,使用 Python 开发,可以确保在出现意外情况时,进程能够持续运行,并在失败后自动重启,通常用于监控服务器进程,如 Web 服务器和应用程序服务器。</p><p>Pros:</p><ol><li>自动重启:当进程意外退出或崩溃时,Supervisor 会自动重启进程,确保进程持续运行,减少因意外情况导致的服务中断时间。</li><li>监控和日志:Supervisor 可以监控进程的状态,并提供详细的日志和报告。</li><li>配置简单:配置文件采用 INI 格式,使用简单,可以根据需要定义进程的启动命令、运行参数、日志路径等。</li><li>多进程管理:Supervisor 可以同时管理多个进程,可以方便地添加、删除和管理多个进程。</li><li>Web 界面:Supervisor 提供了一个简单易用的 Web 界面,用户可以通过浏览器直观地查看和管理进程,包括启动、停止、重启和查看日志等操作。</li></ol><p>Cons:</p><ol><li>Web 界面过于粗糙,只有启停、查看日志等操作,进程 CPU 占用、内存使用等都没有。</li></ol><p>不过说起来这是我第二次使用 supervisor 了,第一次还是几年前,东西都忘了。这次使用的时候查了好多资料才正常跑起来,所以为了方便下一次使用,以及有需要的同道们,特此记录一下,后面有什么新的需要记录的再更新。</p><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>Supervisor 是一个 python package,所以对 pythonista 来说,安装方式再熟悉不过了:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install supervisor</span><br></pre></td></tr></table></figure><h2 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h2><p>配置文件用来配置 supervisor 本身的一些设置以及你要添加的程序,一般叫 <code>supervisord.conf</code> 。如果你启动的时候没有用 <code>-c</code> 来指定配置文件的地址,那么 supervisor 会自动按照以下顺序来寻找:</p><ol><li><code>../etc/supervisord.conf</code> (Relative to the executable)</li><li><code>../supervisord.conf</code> (Relative to the executable)</li><li><code>$CWD/supervisord.conf</code></li><li><code>$CWD/etc/supervisord.conf</code></li><li><code>/etc/supervisord.conf</code></li><li><code>/etc/supervisor/supervisord.conf</code> (since Supervisor 3.3.0)</li></ol><p>可配置项很多,我一般不会从头写,都是基于默认配置来修改。我们可以用 <code>echo_supervisord_conf > supervisord.conf</code> 来将默认配置写到 <code>supervisord.conf</code> 中。</p><div class="alert warning"><p>如果在语句后直接添加注释,那么必须与语句隔一个空格。比如<br>❌ <code>"a=b;comment"</code><br>✔️ <code>"a=b ;comment"</code></p></div><p>下面我就讲下和默认配置不一样的地方。</p><h3 id="inet-http-server"><a href="#inet-http-server" class="headerlink" title="inet_http_server"></a><code>inet_http_server</code></h3><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[inet_http_server]</span> <span class="comment">; inet (TCP) server disabled by default</span></span><br><span class="line"><span class="attr">port</span>=*:<span class="number">9001</span> <span class="comment">; ip_address:port specifier, *:port for all iface</span></span><br><span class="line"><span class="attr">username</span>=your_username <span class="comment">; default is no username (open server)</span></span><br><span class="line"><span class="attr">password</span>=your_password <span class="comment">; default is no password (open server)</span></span><br></pre></td></tr></table></figure><p>这个 section 与后面的 web 界面以及 supervisorctl 与 supervisord 的通信有关。默认是只能本地连接(localhost),而且没有用户名密码,如果需要从其他服务器上访问 web,那么则需要使用 <code>*</code> 来指定允许所有 ip 访问。为了安全考虑建议设置用户名密码。</p><h3 id="supervisorctl"><a href="#supervisorctl" class="headerlink" title="supervisorctl"></a><code>supervisorctl</code></h3><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[supervisorctl]</span></span><br><span class="line"><span class="attr">serverurl</span>=unix:///tmp/supervisor.sock <span class="comment">; use a unix:// URL for a unix socket</span></span><br><span class="line"><span class="attr">serverurl</span>=http://<span class="number">127.0</span>.<span class="number">0.1</span>:<span class="number">9001</span> <span class="comment">; use an http:// url to specify an inet socket</span></span><br><span class="line"><span class="attr">username</span>=liyajun <span class="comment">; should be same as in [*_http_server] if set</span></span><br><span class="line"><span class="attr">password</span>=liyajun123 <span class="comment">; should be same as in [*_http_server] if set</span></span><br></pre></td></tr></table></figure><p>这里就是配置 supervisorctl 与 supervisord 通信的地方。主要是 <code>serverurl</code> (端口)与用户名密码需要和上面配置的一致,否则会出现拒绝链接或者认证失败的错误。</p><h3 id="program-x"><a href="#program-x" class="headerlink" title="program:x"></a><code>program:x</code></h3><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[program:your_program_name]</span></span><br><span class="line"><span class="attr">command</span>=your_program_run_command</span><br><span class="line"><span class="attr">process_name</span>=%(program_name)s</span><br><span class="line"><span class="attr">directory</span>=your_program_run_dir</span><br><span class="line"><span class="attr">autostart</span>=<span class="literal">true</span></span><br><span class="line"><span class="attr">autorestart</span>=unexpected</span><br><span class="line"><span class="attr">redirect_stderr</span>=<span class="literal">true</span></span><br><span class="line"><span class="attr">stdout_logfile</span>=your_program_log <span class="comment">; stdout log path, NONE for none; default AUTO</span></span><br><span class="line"><span class="attr">stdout_logfile_maxbytes</span>=<span class="number">50</span>MB <span class="comment">; max # logfile bytes b4 rotation (default 50MB)</span></span><br><span class="line"><span class="attr">stdout_logfile_backups</span>=<span class="number">10</span> <span class="comment">; # of stdout logfile backups (0 means none, default 10)</span></span><br></pre></td></tr></table></figure><p>这个 section 就是重头戏了,我们在这里配置需要启动的程序,一个配置文件可以有多个这个 section。</p><ul><li><code>[program:your_program_name]</code> :<code>your_program_name</code> 就是你的程序名字,想叫什么就叫什么,别太离谱就行。</li><li><code>command</code> :你的程序的执行命令,你平常怎么执行的这里就怎么写,但是 executable 最好写绝对地址,比如你用 <code>python app.py</code> 来执行程序,那么这个 <code>python</code> 最好写成绝对地址,尤其你有多个 python 环境时。你可以用 <code>which python</code> 来查看绝对地址。</li><li><code>process_name</code> :默认就是程序名(<code>your_program_name</code>),这个名字会在 web 界面上显示。</li><li><code>directory</code> :指定运行时需要 cd 到的目录(工作目录),也即你的程序文件所在的目录。</li><li><code>autostart</code> :是否在 supervisord 启动时启动程序。</li><li><code>autorestart</code> :是否在程序退出时自动重启。有三个值可选:<ul><li><code>false</code> :不自动重启。</li><li><code>unexpected</code> :默认值。当 exit code 不在预料之内时,重启。什么叫不在预料之内?这个值是由 <code>exitcodes</code> 指定的,默认为 0。</li><li><code>true</code> :无论如何都自动重启。</li></ul></li><li><code>redirect_stderr</code> :是否将 stderr redirect 到 stdout。</li><li><code>stdout_logfile</code> :stdout 日志文件。</li><li><code>stdout_logfile_maxbytes</code> :单个日志文件的最大大小。</li><li><code>stdout_logfile_backups</code> :保留多少份日志文件。</li></ul><h2 id="启动"><a href="#启动" class="headerlink" title="启动"></a>启动</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">supervisord -c supervisord.conf</span><br></pre></td></tr></table></figure><h2 id="更新配置文件"><a href="#更新配置文件" class="headerlink" title="更新配置文件"></a>更新配置文件</h2><p>当我们更新了配置文件后,需要让 supervisor 也更新一下,我在网上查到的说是需要先 reread 再 update,但是我查询了<a href="http://supervisord.org/running.html#supervisorctl-actions">文档</a>,文档是这么写的:</p><p><code>reread</code>:</p><blockquote><p>Reload the daemon’s configuration files, without add/remove (no restarts)</p></blockquote><p><code>update</code>:</p><blockquote><p>Reload config and add/remove as necessary, and will restart affected programs</p></blockquote><p>很明显 update 已经做了 reread 的工作,而且还会 restart。</p><p>所以更新配置文件后,<strong>只需执行 <code>update</code></strong> 即可更新 supervisor:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">supervisorctl update</span><br></pre></td></tr></table></figure><p>如果你的配置文件移动了位置,那么需要重启 supervisord:</p><ol><li>查询 supervisord PID:<code>supervisorctl pid</code> 。</li><li>杀死 supervisord 进程:<code>kill supervisord_pid</code> 。</li><li>用新配置文件启动 supervisord:<code>supervisord -c new_supervisord.conf</code> 。</li></ol><h2 id="Web-管理界面"><a href="#Web-管理界面" class="headerlink" title="Web 管理界面"></a>Web 管理界面</h2><p>前面提到过有一个 web 管理界面,根据 <code>inet_http_server</code> 的配置,使用相应的 ip 和端口在浏览器上访问即可,默认是 9001 端口。界面样式如下:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/07/02/jqBiGRF5dEwTusA.png" title="Supervisor web 管理界面。" data-caption="Supervisor web 管理界面。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/07/02/jqBiGRF5dEwTusA.png" alt="Supervisor web 管理界面。"></a><span class="caption">Supervisor web 管理界面。</span></div><p>在这个界面上可以看到所有已配置程序的状态,可以启停和查看日志,直接点击 Name 列可以查看最后几行的日志,而点击 <code>Tail -f *</code> 可以看到实时日志,不过这个在我这里特别慢,不知道为社么。</p><h2 id="supervisord-与-supervisorctl"><a href="#supervisord-与-supervisorctl" class="headerlink" title="supervisord 与 supervisorctl"></a><code>supervisord</code> 与 <code>supervisorctl</code></h2><p>刚才我们用了 supervisord 和 supervisorctl 两个命令,你可能会疑惑这两个有什么区别。其实我们一般使用的时候,用前者启动 supervisor 主程序,用后者来管理我们自己所添加的 program。所以 supervisord 就像个后台的 server,而 supervisorctl 是一个前台的 client,有很多 <a href="http://supervisord.org/running.html#supervisorctl-actions">action</a>(subcommand)可以执行,比如上面的 update 和 reread 就是两个 action。而 supervisord 是没有的。</p><p>supervisorctl 常用的 action 有:</p><ul><li><code>status</code> :查看 program 状态。</li><li><code>update</code> :更新配置文件并重启相关 program。</li><li><code>pid</code> :获取 supervisord 的 PID。</li></ul><p>其他不是很常用,感兴趣的可以去<a href="http://supervisord.org/running.html#supervisorctl-actions">文档</a>查看。</p><h2 id="END"><a href="#END" class="headerlink" title="END"></a>END</h2>]]></content>
<summary type="html"><p>这篇文章介绍了 Supervisor 的功能和优势,它是一个用于管理和监控进程的工具,可以确保进程持续运行并在意外情况下自动重启。它具有简单的配置、多进程管理和提供 Web 界面等特点。虽然 Web 界面有限,但是对于同时运行和维护多个程序的需求非常实用。[ChatGPT]<br></summary>
<category term="Python" scheme="https://alanlee.fun/tags/Python/"/>
<category term="Ubuntu" scheme="https://alanlee.fun/tags/Ubuntu/"/>
</entry>
<entry>
<title>两种方法教你在小米电视上观看 YouTube</title>
<link href="https://alanlee.fun/2023/06/17/xiaomi-tv-youtube/"/>
<id>https://alanlee.fun/2023/06/17/xiaomi-tv-youtube/</id>
<published>2023-06-17T08:01:00.000Z</published>
<updated>2026-04-14T02:51:14.909Z</updated>
<content type="html"><![CDATA[<!-- excerpt --></p><!-- toc --><h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>我一直有个想法是在我家的小米电视上看 YouTube,之前都是手机上下载专门用来投屏 YouTube 的软件(<a href="https://tubioapp.com/">Tubio</a>),缺点是操作稍显复杂,清晰度不能选择,字幕投不上。经过前几天在电视上折腾小白网盘、电视家等经验,这次就想再来试下实现这个想法。</p><h2 id="解决方法"><a href="#解决方法" class="headerlink" title="解决方法"></a>解决方法</h2><p>按理来说,想要看 YouTube,就需要电视上装一个 VPN 软件和 YouTube。电视也是基于 Android 系统做的,所以还比较好实现。VPN 软件我们就可以使用和手机上一样的 ssr 或者 clash。我大概讲下我测试的两种方法。</p><h3 id="方法1:SSR-SmartTube"><a href="#方法1:SSR-SmartTube" class="headerlink" title="方法1:SSR + SmartTube"></a>方法1:SSR + SmartTube</h3><p>这个方法是我看网上传的最多的,但是首先说下这个方法我失败了。</p><p><a href="https://smartyoutubetv.github.io/">SmartTube</a>(<a href="https://github.com/yuliskov/SmartTubeNext">GitHub</a>)是一个专门用于在电视上观看 YouTube 的免广告应用,那为什么不用官方的 YouTube app 呢?因为官方的需要依赖 Google 服务框架那一套东西,比较麻烦,而这个 app 是不需要的。其有 stable 和 beta 两个版本,下载 stable 即可。</p><ol><li>下载 <a href="https://github.com/shadowsocksrr/shadowsocksr-android/releases">SSR apk</a> 和 <a href="https://github.com/yuliskov/SmartTubeNext/releases/download/latest/smarttube_stable.apk">SmartTube apk</a> 到 U 盘上,然后插到电视上,分别安装。</li><li>配置好你的 SSR,测试下看能不能正常连接。我的就在这步卡住了,根本连接不了,订阅是更新成功了,但是选择节点后测试失败。</li><li>假设你上一步成功了,安装并打开 SmartTube,能正常加载首页就表示成功了,你也可以选择登陆自己的账号,点上方的小人根据提示操作即可。</li></ol><h3 id="方法-2:一台挂着梯子的电脑-SmartTube"><a href="#方法-2:一台挂着梯子的电脑-SmartTube" class="headerlink" title="方法 2:一台挂着梯子的电脑 + SmartTube"></a>方法 2:一台挂着梯子的电脑 + SmartTube</h3><p>这个是我成功的方法,也是我认为最方便最简洁的方法,搜索的时候在一个 YouTube 视频看到的。</p><ol><li><p>将电脑梯子设置为局域网可访问。</p> <div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/06/17/K5dCY7TEomPcWkb.png" title="允许局域网连接。" data-caption="允许局域网连接。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/06/17/K5dCY7TEomPcWkb.png" alt="允许局域网连接。"></a><span class="caption">允许局域网连接。</span></div></li><li><p>使用 <code>ifconfig</code> 或者梯子控制台界面找到电脑的 ip 和端口。</p> <div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/06/17/HFOZ5EL8pSXfKas.png" title="查看并记住你的本机地址。" data-caption="查看并记住你的本机地址。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/06/17/HFOZ5EL8pSXfKas.png" alt="查看并记住你的本机地址。"></a><span class="caption">查看并记住你的本机地址。</span></div> <div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/06/17/Z1CxhfYMdpEqmAj.png" title="查看并记住代理端口。" data-caption="查看并记住代理端口。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/06/17/Z1CxhfYMdpEqmAj.png" alt="查看并记住代理端口。"></a><span class="caption">查看并记住代理端口。</span></div></li><li><p>在 SmartTube 上,在左侧选择 <code>设置</code> ➡️ <code>一般</code> ➡️ <code>互联网审查</code> ➡️ <code>使用网页代理</code>,根据提示填入刚才的 ip 和端口即可,代理类型选择 <code>HTTP</code>,测试一下,两个都 OK 就表示成功连接了代理。</p> <div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/06/17/fGWvwHbglIhuCPi.png" title="设置让 SmartTube 使用网页代理。" data-caption="设置让 SmartTube 使用网页代理。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/06/17/fGWvwHbglIhuCPi.png" alt="设置让 SmartTube 使用网页代理。"></a><span class="caption">设置让 SmartTube 使用网页代理。</span></div> <div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/06/17/ZnklpoJBcgeCvjV.png" title="填入 ip 和端口。" data-caption="填入 ip 和端口。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/06/17/ZnklpoJBcgeCvjV.png" alt="填入 ip 和端口。"></a><span class="caption">填入 ip 和端口。</span></div></li><li><p>确定退出回到首页,就可以看到能成功加载了。登录账号啥的和方法 1 相同,也可以选择清晰度和字幕,和电脑上几乎一样,非常方便。</p> <div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/06/17/1KdNQcvfD6HBMs8.png" title="首页成功加载。" data-caption="首页成功加载。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/06/17/1KdNQcvfD6HBMs8.png" alt="首页成功加载。"></a><span class="caption">首页成功加载。</span></div></li></ol><p>通过以上两种方法,你就可以方便地在小米电视上观看 YouTube,享受大屏幕带来的视觉效果啦~</p><h2 id="END"><a href="#END" class="headerlink" title="END"></a>END</h2>]]></content>
<summary type="html"><p>本文介绍了两种在小米电视上观看 YouTube 的方法:SSR + SmartTube 和一台挂着梯子的电脑 + SmartTube。通过这两种方法,用户能够方便地在小米电视上观看 YouTube,并享受大屏幕带来的视觉效果。[ChatGPT]<br></summary>
<category term="Tools" scheme="https://alanlee.fun/tags/Tools/"/>
<category term="Misc" scheme="https://alanlee.fun/tags/Misc/"/>
<category term="Living" scheme="https://alanlee.fun/tags/Living/"/>
</entry>
<entry>
<title>批量导出 QQ 邮箱邮件</title>
<link href="https://alanlee.fun/2023/05/26/batch-export-qq-emails/"/>
<id>https://alanlee.fun/2023/05/26/batch-export-qq-emails/</id>
<published>2023-05-26T01:21:00.000Z</published>
<updated>2026-04-14T02:51:14.899Z</updated>
<content type="html"><</span><br><span class="line">To: [recipient@example.com](mailto:recipient@example.com)</span><br><span class="line">Subject: This <span class="keyword">is</span> a test email</span><br><span class="line"></span><br><span class="line">This <span class="keyword">is</span> the body of the email.</span><br></pre></td></tr></table></figure><p>在 Python 中读取一个 eml 文件,可以使用 Python 内置的 email 模块。下面是一个读取 eml 文件并打印邮件头部和正文内容的 demo 代码:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> email</span><br><span class="line"></span><br><span class="line"><span class="keyword">with</span> <span class="built_in">open</span>(<span class="string">'example.eml'</span>, <span class="string">'rb'</span>) <span class="keyword">as</span> fp:</span><br><span class="line"> msg = email.message_from_binary_file(fp)</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="string">'From:'</span>, msg[<span class="string">'From'</span>])</span><br><span class="line"><span class="built_in">print</span>(<span class="string">'To:'</span>, msg[<span class="string">'To'</span>])</span><br><span class="line"><span class="built_in">print</span>(<span class="string">'Subject:'</span>, msg[<span class="string">'Subject'</span>])</span><br><span class="line"><span class="built_in">print</span>(<span class="string">'Body:'</span>, msg.get_payload())</span><br></pre></td></tr></table></figure><p>mbox 格式是另一种常见的存储邮件的文件格式,其英文全称为 “mailbox format”。一个 mbox 文件通常包括多个邮件,每个邮件之间用一个特殊的分隔符隔开,可以使用邮件客户端或者文本编辑器打开。下面是一个 mbox 文件的简单示例:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">From sender@example.com Wed May <span class="number">18</span> <span class="number">13</span>:<span class="number">48</span>:<span class="number">45</span> <span class="number">2022</span></span><br><span class="line">Return-Path: <sender@example.com></span><br><span class="line">X-Spam-Checker-Version: SpamAssassin <span class="number">3.4</span><span class="number">.2</span> (<span class="number">2018</span>-09-<span class="number">13</span>) on example.com</span><br><span class="line">...</span><br><span class="line">This <span class="keyword">is</span> the body of the email.</span><br><span class="line"></span><br><span class="line">From recipient@example.com Wed May <span class="number">18</span> <span class="number">14</span>:03:<span class="number">10</span> <span class="number">2022</span></span><br><span class="line">Return-Path: <recipient@example.com></span><br><span class="line">X-Spam-Checker-Version: SpamAssassin <span class="number">3.4</span><span class="number">.2</span> (<span class="number">2018</span>-09-<span class="number">13</span>) on example.com</span><br><span class="line">...</span><br><span class="line">This <span class="keyword">is</span> another email.</span><br></pre></td></tr></table></figure><p>在 Python 中读取一个 mbox 文件,可以使用 Python 内置的 mailbox 模块。下面是一个读取 mbox 文件并打印每个邮件的头部和正文内容的 demo 代码:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> mailbox</span><br><span class="line"></span><br><span class="line">mbox = mailbox.mbox(<span class="string">'example.mbox'</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> msg <span class="keyword">in</span> mbox:</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'From:'</span>, msg[<span class="string">'From'</span>])</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'To:'</span>, msg[<span class="string">'To'</span>])</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'Subject:'</span>, msg[<span class="string">'Subject'</span>])</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">'Body:'</span>, msg.get_payload())</span><br></pre></td></tr></table></figure><h2 id="END"><a href="#END" class="headerlink" title="END"></a>END</h2>]]></content>
<summary type="html"><p>本文介绍了如何批量导出 QQ 邮箱邮件的方法,需要把“收取选项”改为“全部”才能导出所有邮件。同时介绍了 eml 和 mbox 两种邮件格式的知识和在 Python 中读取邮件的方法。[ChatGPT]<br></summary>
<category term="Misc" scheme="https://alanlee.fun/tags/Misc/"/>
</entry>
<entry>
<title>2023 五一北京周边行 3/3 —— 北京野生动物园</title>
<link href="https://alanlee.fun/2023/05/17/travel-international-workers-day-2023-Beijing-Wildlife-Park/"/>
<id>https://alanlee.fun/2023/05/17/travel-international-workers-day-2023-Beijing-Wildlife-Park/</id>
<published>2023-05-17T09:15:00.000Z</published>
<updated>2026-04-14T02:51:14.907Z</updated>
<content type="html"><![CDATA[<!-- excerpt --><!-- toc --><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><blockquote><p>由于图片较多,所以将分三篇文章发出。本文是第三篇。其他两篇见<a href="https://alanlee.fun/2023/05/17/travel-international-workers-day-2023-Jinhai-Lake/">第一篇</a>和<a href="https://alanlee.fun/2023/05/17/travel-international-workers-day-2023-Yeya-Lake/">第二篇</a>。</p></blockquote><p>见<a href="https://alanlee.fun/2023/05/17/travel-international-workers-day-2023-Jinhai-Lake/">第一篇</a>前言。</p><h2 id="北京野生动物园"><a href="#北京野生动物园" class="headerlink" title="北京野生动物园"></a>北京野生动物园</h2><p>北野也是一个很久之前就在收藏夹里的地方。北京有两个野生动物园,之前我还有点分不清,现在终于搞清了,一个是八达岭野生动物园,一个就是位于大兴的北京野生动物园。前者就是发生了私自下车导致被老虎拖走事件的动物园。</p><p>经过在小红书上一段时间的有针对性的冲浪,发现大家去那主要奔着两个地方:猛兽区和小火车。两者都是有优速通的,不买的话旺季(周末节假日)可能要排队几个小时,最夸张的是有个人说活排了 6 个小时……</p><p>我们这次主要奔着猛兽区,剩余的地方随便闲逛。我们早上 5 点起床,6 点出发,7 点 10 分左右到达北区停车场,南区似乎已经满了。不过南北区距离大门口都差不多。7 点 45 进入大门。</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/xVbJlYPGcAK6mWw.png" title="北野临时停车场之一。在快到北野的时候,路上有好几个这种牌子,应该是南区北区停满之后就要往这里引导了。之前看公众号上说南区北区 + 临时停车场总共有大概 8000 左右车位,可太夸张了。" data-caption="北野临时停车场之一。在快到北野的时候,路上有好几个这种牌子,应该是南区北区停满之后就要往这里引导了。之前看公众号上说南区北区 + 临时停车场总共有大概 8000 左右车位,可太夸张了。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/xVbJlYPGcAK6mWw.png" alt="北野临时停车场之一。在快到北野的时候,路上有好几个这种牌子,应该是南区北区停满之后就要往这里引导了。之前看公众号上说南区北区 + 临时停车场总共有大概 8000 左右车位,可太夸张了。"></a><span class="caption">北野临时停车场之一。在快到北野的时候,路上有好几个这种牌子,应该是南区北区停满之后就要往这里引导了。之前看公众号上说南区北区 + 临时停车场总共有大概 8000 左右车位,可太夸张了。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/MhWTBkgy4UcDNPK.png" title="排队安检。注意抖音买的票需要换票,携程等买的不需要。" data-caption="排队安检。注意抖音买的票需要换票,携程等买的不需要。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/MhWTBkgy4UcDNPK.png" alt="排队安检。注意抖音买的票需要换票,携程等买的不需要。"></a><span class="caption">排队安检。注意抖音买的票需要换票,携程等买的不需要。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/5jB7AmLe4YarH3q.png" title="大门。" data-caption="大门。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/5jB7AmLe4YarH3q.png" alt="大门。"></a><span class="caption">大门。</span></div><p>接下来就是直冲猛兽区了。先说下排队盛况,排队确实是无限回形针。先是木栈道排队,然后进入蓝房子(河马馆),此处大概要拐六七道。然后通过一个羊肠小道进入一个大型回形针区域,此处大概要拐十几道。然后进入最后一个区域,此时需要存车了,到这就快了,同样是回形针区域,慢慢排就是了,上面屏幕上可能会写“此处排队时间 60 分钟”之类的的话,这个预估似乎偏大。</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/NHVhs8AlPJT6LdK.png" title="木栈道排队。" data-caption="木栈道排队。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/NHVhs8AlPJT6LdK.png" alt="木栈道排队。"></a><span class="caption">木栈道排队。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/bSodj9X8VDxzE52.png" title="河马馆排队。里面真的有河马,所以味道很冲。" data-caption="河马馆排队。里面真的有河马,所以味道很冲。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/bSodj9X8VDxzE52.png" alt="河马馆排队。里面真的有河马,所以味道很冲。"></a><span class="caption">河马馆排队。里面真的有河马,所以味道很冲。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/CVSBZGIpoQt43AJ.png" title="曙光。到这里就快了,准备好钱买肉吧!" data-caption="曙光。到这里就快了,准备好钱买肉吧!" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/CVSBZGIpoQt43AJ.png" alt="曙光。到这里就快了,准备好钱买肉吧!"></a><span class="caption">曙光。到这里就快了,准备好钱买肉吧!</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/aDNB3ecMiWkpJPy.png" title="笼车和肉。一杯 50,包括牛肉和鸡肉。9 点 40 左右坐上车,排队一个小时四十分钟左右。" data-caption="笼车和肉。一杯 50,包括牛肉和鸡肉。9 点 40 左右坐上车,排队一个小时四十分钟左右。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/aDNB3ecMiWkpJPy.png" alt="笼车和肉。一杯 50,包括牛肉和鸡肉。9 点 40 左右坐上车,排队一个小时四十分钟左右。"></a><span class="caption">笼车和肉。一杯 50,包括牛肉和鸡肉。9 点 40 左右坐上车,排队一个小时四十分钟左右。</span></div><p>网上总有人说坐哪边猛兽多之类的,但左右都差不多,棕熊区右边看到的是湖水,左边是瀑布。其他动物左右均有,司机也会提醒动物在哪一边。你也可以站起来去有动物的一边拍照喂食。司机也会讲解提醒哪个动物喜欢吃什么肉。</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/SB1d4IsvKUXNatE.png" title="棕熊区。就是围着这个小池子转,每辆车都会在图中蓝色和粉色车子那里停,大概得个十几分钟。水里陆地都有棕熊。有电网限制它们的活动区域。" data-caption="棕熊区。就是围着这个小池子转,每辆车都会在图中蓝色和粉色车子那里停,大概得个十几分钟。水里陆地都有棕熊。有电网限制它们的活动区域。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/SB1d4IsvKUXNatE.png" alt="棕熊区。就是围着这个小池子转,每辆车都会在图中蓝色和粉色车子那里停,大概得个十几分钟。水里陆地都有棕熊。有电网限制它们的活动区域。"></a><span class="caption">棕熊区。就是围着这个小池子转,每辆车都会在图中蓝色和粉色车子那里停,大概得个十几分钟。水里陆地都有棕熊。有电网限制它们的活动区域。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/GnNaKgTrOo9uxHf.png" title="棕熊。棕熊是主要的食物消耗者,有的棕熊只吃牛肉,不吃鸡肉。司机会建议你能喂多喂,到后面你可能肉都抛不出去了。" data-caption="棕熊。棕熊是主要的食物消耗者,有的棕熊只吃牛肉,不吃鸡肉。司机会建议你能喂多喂,到后面你可能肉都抛不出去了。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/GnNaKgTrOo9uxHf.png" alt="棕熊。棕熊是主要的食物消耗者,有的棕熊只吃牛肉,不吃鸡肉。司机会建议你能喂多喂,到后面你可能肉都抛不出去了。"></a><span class="caption">棕熊。棕熊是主要的食物消耗者,有的棕熊只吃牛肉,不吃鸡肉。司机会建议你能喂多喂,到后面你可能肉都抛不出去了。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/EXeFzCkDbat1qmc.png" title="棕熊信步。从后门拍摄。" data-caption="棕熊信步。从后门拍摄。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/EXeFzCkDbat1qmc.png" alt="棕熊信步。从后门拍摄。"></a><span class="caption">棕熊信步。从后门拍摄。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/pirkh3bQTOvoHtX.png" title="水虎。非常幸运,这只老虎最后从水里出来找我们要肉了。" data-caption="水虎。非常幸运,这只老虎最后从水里出来找我们要肉了。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/pirkh3bQTOvoHtX.png" alt="水虎。非常幸运,这只老虎最后从水里出来找我们要肉了。"></a><span class="caption">水虎。非常幸运,这只老虎最后从水里出来找我们要肉了。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/uIgWDCPQ4NK7mAM.png" title="白虎。由于其毛色在野外太扎眼,据说已经没有野生白虎了。" data-caption="白虎。由于其毛色在野外太扎眼,据说已经没有野生白虎了。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/uIgWDCPQ4NK7mAM.png" alt="白虎。由于其毛色在野外太扎眼,据说已经没有野生白虎了。"></a><span class="caption">白虎。由于其毛色在野外太扎眼,据说已经没有野生白虎了。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/3swPTbWUJVkrmfc.png" title="夹着尾巴跑的狼。" data-caption="夹着尾巴跑的狼。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/3swPTbWUJVkrmfc.png" alt="夹着尾巴跑的狼。"></a><span class="caption">夹着尾巴跑的狼。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/8zwrfSvsk7L6lA3.png" title="工作车。这车是早晚用来释放和回收动物的。那它现在怎么停在这?据司机说,可能是因为这些狼之前在打架,他们害怕这些车,也就是在它们眼里这个车就是狼王,狼王一来都不敢动了,起一个震慑作用。" data-caption="工作车。这车是早晚用来释放和回收动物的。那它现在怎么停在这?据司机说,可能是因为这些狼之前在打架,他们害怕这些车,也就是在它们眼里这个车就是狼王,狼王一来都不敢动了,起一个震慑作用。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/8zwrfSvsk7L6lA3.png" alt="工作车。这车是早晚用来释放和回收动物的。那它现在怎么停在这?据司机说,可能是因为这些狼之前在打架,他们害怕这些车,也就是在它们眼里这个车就是狼王,狼王一来都不敢动了,起一个震慑作用。"></a><span class="caption">工作车。这车是早晚用来释放和回收动物的。那它现在怎么停在这?据司机说,可能是因为这些狼之前在打架,他们害怕这些车,也就是在它们眼里这个车就是狼王,狼王一来都不敢动了,起一个震慑作用。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/Gx4jv6HDOYo9KfZ.png" title="黑熊。" data-caption="黑熊。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/Gx4jv6HDOYo9KfZ.png" alt="黑熊。"></a><span class="caption">黑熊。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/CAtah5QrJUY6KM9.png" title="狮子。" data-caption="狮子。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/CAtah5QrJUY6KM9.png" alt="狮子。"></a><span class="caption">狮子。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/iy2n1wpxoOCXHSc.png" title="唯一在动的猎豹。" data-caption="唯一在动的猎豹。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/iy2n1wpxoOCXHSc.png" alt="唯一在动的猎豹。"></a><span class="caption">唯一在动的猎豹。</span></div><p>然后就从猛兽区出来了。出来后有个餐厅,里面是只能在他们那买东西之后才能坐,外面是都可以坐。</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/RYCHlej5IEF2QxG.png" title="未知蓝房子。似乎是一个剧场,很多人在这里拍照,容易出片。" data-caption="未知蓝房子。似乎是一个剧场,很多人在这里拍照,容易出片。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/RYCHlej5IEF2QxG.png" alt="未知蓝房子。似乎是一个剧场,很多人在这里拍照,容易出片。"></a><span class="caption">未知蓝房子。似乎是一个剧场,很多人在这里拍照,容易出片。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/qvFdZrtnXu8kEKj.png" title="犀鸟。在这个屋里扑棱扑棱从这头飞到那头,有点怕扑棱到自己哈哈哈。" data-caption="犀鸟。在这个屋里扑棱扑棱从这头飞到那头,有点怕扑棱到自己哈哈哈。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/qvFdZrtnXu8kEKj.png" alt="犀鸟。在这个屋里扑棱扑棱从这头飞到那头,有点怕扑棱到自己哈哈哈。"></a><span class="caption">犀鸟。在这个屋里扑棱扑棱从这头飞到那头,有点怕扑棱到自己哈哈哈。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/rm7HaBb6nvLiSGZ.png" title="惠风和畅。" data-caption="惠风和畅。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/rm7HaBb6nvLiSGZ.png" alt="惠风和畅。"></a><span class="caption">惠风和畅。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/s2LxkPuOGvqepVN.png" title="怡然自乐。这两张图让我想起了日本那个景点,也是两边都是这种红色柱子。" data-caption="怡然自乐。这两张图让我想起了日本那个景点,也是两边都是这种红色柱子。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/s2LxkPuOGvqepVN.png" alt="怡然自乐。这两张图让我想起了日本那个景点,也是两边都是这种红色柱子。"></a><span class="caption">怡然自乐。这两张图让我想起了日本那个景点,也是两边都是这种红色柱子。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/J2X1cnY5ahf9BSv.png" title="奇妙旅程票价。" data-caption="奇妙旅程票价。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/J2X1cnY5ahf9BSv.png" alt="奇妙旅程票价。"></a><span class="caption">奇妙旅程票价。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/sWbxDnHArQmwpE2.png" title="犀牛。" data-caption="犀牛。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/sWbxDnHArQmwpE2.png" alt="犀牛。"></a><span class="caption">犀牛。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/EfYxcWSD3ZzQJyk.png" title="大象喂食。每人 30,旁边的那个细白线是电网。" data-caption="大象喂食。每人 30,旁边的那个细白线是电网。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/EfYxcWSD3ZzQJyk.png" alt="大象喂食。每人 30,旁边的那个细白线是电网。"></a><span class="caption">大象喂食。每人 30,旁边的那个细白线是电网。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/zeigAusV7NWMOIh.png" title="长颈鹿。喂食价格同上。" data-caption="长颈鹿。喂食价格同上。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/zeigAusV7NWMOIh.png" alt="长颈鹿。喂食价格同上。"></a><span class="caption">长颈鹿。喂食价格同上。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/kQw1cXyvRLZSsjm.png" title="小熊猫馆。人很多,要来的话趁早。" data-caption="小熊猫馆。人很多,要来的话趁早。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/kQw1cXyvRLZSsjm.png" alt="小熊猫馆。人很多,要来的话趁早。"></a><span class="caption">小熊猫馆。人很多,要来的话趁早。</span></div><p>顺便说几个关于熊猫的冷知识:</p><ol><li>小熊猫和大熊猫没有关系。</li><li>小熊猫比大熊猫更为“濒危”。</li><li>panda 最初指小熊猫,而大熊猫叫 giant panda 。</li><li>bearcat 指另一个物种熊狸。</li><li>猫熊比大熊猫更准确。</li></ol><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/6TqicFlwgGEsk2p.png" title="树人。门口有很多“参天大树”。" data-caption="树人。门口有很多“参天大树”。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/6TqicFlwgGEsk2p.png" alt="树人。门口有很多“参天大树”。"></a><span class="caption">树人。门口有很多“参天大树”。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/LHvSZXiV9REsITU.png" title="有趣的狐獴。" data-caption="有趣的狐獴。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/LHvSZXiV9REsITU.png" alt="有趣的狐獴。"></a><span class="caption">有趣的狐獴。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/K9kBnfFD1rlVLIs.png" title="说个题外话,Mac 自带输入法似乎打不出「獴」字。" data-caption="说个题外话,Mac 自带输入法似乎打不出「獴」字。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/K9kBnfFD1rlVLIs.png" alt="说个题外话,Mac 自带输入法似乎打不出「獴」字。"></a><span class="caption">说个题外话,Mac 自带输入法似乎打不出「獴」字。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/5Ml3LHhAkGmIO7v.png" title="有趣的狐獴 2。" data-caption="有趣的狐獴 2。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/5Ml3LHhAkGmIO7v.png" alt="有趣的狐獴 2。"></a><span class="caption">有趣的狐獴 2。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/2oeuUD3gAOEJZGR.png" title="北线游览区结束点。" data-caption="北线游览区结束点。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/2oeuUD3gAOEJZGR.png" alt="北线游览区结束点。"></a><span class="caption">北线游览区结束点。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/onrbdpwEKfk2atl.png" title="霸王龙与火烈鸟。火烈鸟真的很好看,强烈推荐,北京动物园里有个火烈鸟展区,超漂亮!" data-caption="霸王龙与火烈鸟。火烈鸟真的很好看,强烈推荐,北京动物园里有个火烈鸟展区,超漂亮!" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/onrbdpwEKfk2atl.png" alt="霸王龙与火烈鸟。火烈鸟真的很好看,强烈推荐,北京动物园里有个火烈鸟展区,超漂亮!"></a><span class="caption">霸王龙与火烈鸟。火烈鸟真的很好看,强烈推荐,北京动物园里有个火烈鸟展区,超漂亮!</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/oI2sexFqHnN6Vij.png" title="水乡。" data-caption="水乡。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/oI2sexFqHnN6Vij.png" alt="水乡。"></a><span class="caption">水乡。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/FL82WjungqBdGOv.png" title="全程轨迹图。海拔在 20 米左右(忽略后面那段)。" data-caption="全程轨迹图。海拔在 20 米左右(忽略后面那段)。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/FL82WjungqBdGOv.png" alt="全程轨迹图。海拔在 20 米左右(忽略后面那段)。"></a><span class="caption">全程轨迹图。海拔在 20 米左右(忽略后面那段)。</span></div><h2 id="END"><a href="#END" class="headerlink" title="END"></a>END</h2>]]></content>
<summary type="html"><p>这篇文章介绍了作者在北京野生动物园游玩的经历,主要聚焦于猛兽区,并分享了一些有趣的动物照片和冷知识。[ChatGPT]</p></summary>
<category term="Living" scheme="https://alanlee.fun/tags/Living/"/>
<category term="Travel" scheme="https://alanlee.fun/tags/Travel/"/>
</entry>
<entry>
<title>2023 五一北京周边行 2/3 —— 野鸭湖国家湿地公园</title>
<link href="https://alanlee.fun/2023/05/17/travel-international-workers-day-2023-Yeya-Lake/"/>
<id>https://alanlee.fun/2023/05/17/travel-international-workers-day-2023-Yeya-Lake/</id>
<published>2023-05-17T08:47:00.000Z</published>
<updated>2026-04-14T02:51:14.907Z</updated>
<content type="html"><与鸟。" alt=""></a></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/MhFpKYd3nO6aLSw.png" title="一张特写的飞机云。恰巧那天碰见了,在蓝天的衬托下显得格外漂亮,我的最爱。" data-caption="一张特写的飞机云。恰巧那天碰见了,在蓝天的衬托下显得格外漂亮,我的最爱。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/MhFpKYd3nO6aLSw.png" alt="一张特写的飞机云。恰巧那天碰见了,在蓝天的衬托下显得格外漂亮,我的最爱。"></a><span class="caption">一张特写的飞机云。恰巧那天碰见了,在蓝天的衬托下显得格外漂亮,我的最爱。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/eS26kUzNrM8XTyE.png" title="瞭望塔近景。" data-caption="瞭望塔近景。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/eS26kUzNrM8XTyE.png" alt="瞭望塔近景。"></a><span class="caption">瞭望塔近景。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/xaBl59On714oKjU.png" title="官厅水库。现在看到的这片水域应该就是官厅水库了。照片同样由我们夸张的小米 11 ultra 贡献。" data-caption="官厅水库。现在看到的这片水域应该就是官厅水库了。照片同样由我们夸张的小米 11 ultra 贡献。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/xaBl59On714oKjU.png" alt="官厅水库。现在看到的这片水域应该就是官厅水库了。照片同样由我们夸张的小米 11 ultra 贡献。"></a><span class="caption">官厅水库。现在看到的这片水域应该就是官厅水库了。照片同样由我们夸张的小米 11 ultra 贡献。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/Wl3LjaEJvfNFrsw.png" title="湖中间的木栈道。这一条长长的木栈道位于湖中间,可以直接从半路穿湖到达对面亲水平台和门口。这条栈道感觉有点年久失修,有些地方翘起来挺危险的,没有任何护栏。走在上面风贼大。" data-caption="湖中间的木栈道。这一条长长的木栈道位于湖中间,可以直接从半路穿湖到达对面亲水平台和门口。这条栈道感觉有点年久失修,有些地方翘起来挺危险的,没有任何护栏。走在上面风贼大。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/Wl3LjaEJvfNFrsw.png" alt="湖中间的木栈道。这一条长长的木栈道位于湖中间,可以直接从半路穿湖到达对面亲水平台和门口。这条栈道感觉有点年久失修,有些地方翘起来挺危险的,没有任何护栏。走在上面风贼大。"></a><span class="caption">湖中间的木栈道。这一条长长的木栈道位于湖中间,可以直接从半路穿湖到达对面亲水平台和门口。这条栈道感觉有点年久失修,有些地方翘起来挺危险的,没有任何护栏。走在上面风贼大。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/o2iywSeVmzf3XsB.png" title="结束。这纯走一圈还真挺累的。" data-caption="结束。这纯走一圈还真挺累的。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/o2iywSeVmzf3XsB.png" alt="结束。这纯走一圈还真挺累的。"></a><span class="caption">结束。这纯走一圈还真挺累的。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/vnP9X13S6MfxuqI.png" title="全程轨迹图。海拔明显高了,大概在 480 米左右。" data-caption="全程轨迹图。海拔明显高了,大概在 480 米左右。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/vnP9X13S6MfxuqI.png" alt="全程轨迹图。海拔明显高了,大概在 480 米左右。"></a><span class="caption">全程轨迹图。海拔明显高了,大概在 480 米左右。</span></div><h2 id="END"><a href="#END" class="headerlink" title="END"></a>END</h2>]]></content>
<summary type="html"><p>本文是第二篇,介绍了北京野鸭湖国家湿地公园的游览体验,包括门口氛围组、亲鹿苑、木栈道等景点,以及景区全景图和轨迹图。[ChatGPT]</p></summary>
<category term="Living" scheme="https://alanlee.fun/tags/Living/"/>
<category term="Travel" scheme="https://alanlee.fun/tags/Travel/"/>
</entry>
<entry>
<title>2023 五一北京周边行 1/3 —— 北京金海湖风景区</title>
<link href="https://alanlee.fun/2023/05/17/travel-international-workers-day-2023-Jinhai-Lake/"/>
<id>https://alanlee.fun/2023/05/17/travel-international-workers-day-2023-Jinhai-Lake/</id>
<published>2023-05-17T05:34:00.000Z</published>
<updated>2026-04-14T02:51:14.907Z</updated>
<content type="html"><女儿)墓。" alt=""></a></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/kagGsZ7M19mDBpb.png" title="金花公主墓。" data-caption="金花公主墓。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/kagGsZ7M19mDBpb.png" alt="金花公主墓。"></a><span class="caption">金花公主墓。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/adsASl5vyge8m14.png" title="站在去往金花公主墓的走廊上看金海湖大坝。" data-caption="站在去往金花公主墓的走廊上看金海湖大坝。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/adsASl5vyge8m14.png" alt="站在去往金花公主墓的走廊上看金海湖大坝。"></a><span class="caption">站在去往金花公主墓的走廊上看金海湖大坝。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/LHxownYTOVdstFu.png" title="登上游船去往桃花岛。这是去桃花岛的唯一方式。还有一个碧波岛,打听了才知道从景区内过不去,得从景区外另一个门进去,但是票是一次性的……平常碧波岛应该是可以坐这个游船过去的。船票 130 每人,最后你网上订票的时候订游船 + 门票的联票,因为这个联票价格也是 130。在这里排队人很多,由于工作人员工作不到位(排队方式不清晰),导致我前面的游客还跟工作人员吵了一下。" data-caption="登上游船去往桃花岛。这是去桃花岛的唯一方式。还有一个碧波岛,打听了才知道从景区内过不去,得从景区外另一个门进去,但是票是一次性的……平常碧波岛应该是可以坐这个游船过去的。船票 130 每人,最后你网上订票的时候订游船 + 门票的联票,因为这个联票价格也是 130。在这里排队人很多,由于工作人员工作不到位(排队方式不清晰),导致我前面的游客还跟工作人员吵了一下。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/LHxownYTOVdstFu.png" alt="登上游船去往桃花岛。这是去桃花岛的唯一方式。还有一个碧波岛,打听了才知道从景区内过不去,得从景区外另一个门进去,但是票是一次性的……平常碧波岛应该是可以坐这个游船过去的。船票 130 每人,最后你网上订票的时候订游船 + 门票的联票,因为这个联票价格也是 130。在这里排队人很多,由于工作人员工作不到位(排队方式不清晰),导致我前面的游客还跟工作人员吵了一下。"></a><span class="caption">登上游船去往桃花岛。这是去桃花岛的唯一方式。还有一个碧波岛,打听了才知道从景区内过不去,得从景区外另一个门进去,但是票是一次性的……平常碧波岛应该是可以坐这个游船过去的。船票 130 每人,最后你网上订票的时候订游船 + 门票的联票,因为这个联票价格也是 130。在这里排队人很多,由于工作人员工作不到位(排队方式不清晰),导致我前面的游客还跟工作人员吵了一下。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/oeQf17JTD3sR8tG.png" title="到达桃花岛。这里露营似乎也是收费的。我看到一家人正在搭帐篷,然后走过来一个拿个牌子的工作人员跟其交谈,后来那家人就把帐篷收起来了。这个岛上除了大片草地,还有卖小吃的、小型动物园(鸵鸟、孔雀和兔子等)、小型游乐场。哦对了,这里的烤肠 5 元/根,而景区入口的 10 元。" data-caption="到达桃花岛。这里露营似乎也是收费的。我看到一家人正在搭帐篷,然后走过来一个拿个牌子的工作人员跟其交谈,后来那家人就把帐篷收起来了。这个岛上除了大片草地,还有卖小吃的、小型动物园(鸵鸟、孔雀和兔子等)、小型游乐场。哦对了,这里的烤肠 5 元/根,而景区入口的 10 元。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/oeQf17JTD3sR8tG.png" alt="到达桃花岛。这里露营似乎也是收费的。我看到一家人正在搭帐篷,然后走过来一个拿个牌子的工作人员跟其交谈,后来那家人就把帐篷收起来了。这个岛上除了大片草地,还有卖小吃的、小型动物园(鸵鸟、孔雀和兔子等)、小型游乐场。哦对了,这里的烤肠 5 元/根,而景区入口的 10 元。"></a><span class="caption">到达桃花岛。这里露营似乎也是收费的。我看到一家人正在搭帐篷,然后走过来一个拿个牌子的工作人员跟其交谈,后来那家人就把帐篷收起来了。这个岛上除了大片草地,还有卖小吃的、小型动物园(鸵鸟、孔雀和兔子等)、小型游乐场。哦对了,这里的烤肠 5 元/根,而景区入口的 10 元。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/J73dKy4IxqZMtc1.png" title="金海湖帆船。价格和游船差不多,但是好像很少有人坐,大部分人坐游船,其次是快艇。" data-caption="金海湖帆船。价格和游船差不多,但是好像很少有人坐,大部分人坐游船,其次是快艇。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/J73dKy4IxqZMtc1.png" alt="金海湖帆船。价格和游船差不多,但是好像很少有人坐,大部分人坐游船,其次是快艇。"></a><span class="caption">金海湖帆船。价格和游船差不多,但是好像很少有人坐,大部分人坐游船,其次是快艇。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/amKj3TzqNSXQBuJ.png" title="在坝上看景区入口那块露营地。" data-caption="在坝上看景区入口那块露营地。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/amKj3TzqNSXQBuJ.png" alt="在坝上看景区入口那块露营地。"></a><span class="caption">在坝上看景区入口那块露营地。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/05/17/yK76bWrtqn5fFTV.png" title="全程轨迹图。可以看到海拔不是很高,也就 100 米左右。" data-caption="全程轨迹图。可以看到海拔不是很高,也就 100 米左右。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/05/17/yK76bWrtqn5fFTV.png" alt="全程轨迹图。可以看到海拔不是很高,也就 100 米左右。"></a><span class="caption">全程轨迹图。可以看到海拔不是很高,也就 100 米左右。</span></div><h2 id="END"><a href="#END" class="headerlink" title="END"></a>END</h2>]]></content>
<summary type="html"><p>本文介绍作者五一假期的北京周边三日亲子游行程,通过筛选选择了北京金海湖风景区、野鸭湖国家湿地公园和北京野生动物园,详细介绍了金海湖风景区的游览感受。[ChatGPT]<br></summary>
<category term="Living" scheme="https://alanlee.fun/tags/Living/"/>
<category term="Travel" scheme="https://alanlee.fun/tags/Travel/"/>
</entry>
<entry>
<title>pandas 读取合并单元格并保留合并信息</title>
<link href="https://alanlee.fun/2023/04/27/pandas-read-excel-with-merged-cells/"/>
<id>https://alanlee.fun/2023/04/27/pandas-read-excel-with-merged-cells/</id>
<published>2023-04-27T10:16:00.000Z</published>
<updated>2026-04-14T02:51:14.904Z</updated>
<content type="html"><![CDATA[<!-- excerpt --><!-- toc --><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>当我们使用 pandas 的 <code>read_excel</code> 方法读取 Excel 文件时,我们可能会遇到一个很棘手的问题:如何正确读取包含合并单元格的 Excel 表格。如果我们只是用原先的 <code>read_excel</code> 方法读取,那么合并单元格的信息将会丢失,从而导致我们的数据出现重复或缺失的情况。我看了下网上的文章几乎都没有很好的解决办法,大部分都是用 <code>fillna</code> 之类的方法去填充,很明显这是不行的,下面我会举例说明。唯一看到一篇方向正确的文章,但是却稍显繁琐,还要先存一个中间文件再读取。</p><p>在本篇文章中,我们将会探讨如何使用 pandas 正确地读取包含合并单元格的 Excel 表格,简单高效全面,同时支持 xlsx 和旧格式 xls。</p><p>本篇文章使用两个内容相同、格式不同的文件来演示说明。内容截图如下:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/04/27/bQCG5HpWAuqer92.png" title="样例文件" data-caption="样例文件" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/04/27/bQCG5HpWAuqer92.png" alt="样例文件"></a><span class="caption">样例文件</span></div><p>可以看到里面有纵向合并(一班、二班、三班),有横向合并(钱一的语文和数学),也有横纵合并(二班三班的语文数学)。</p><h2 id="fillna-的问题"><a href="#fillna-的问题" class="headerlink" title="fillna 的问题"></a>fillna 的问题</h2><p>当我们直接使用 <code>read_excel</code> 读取时,会变成下面这个样子:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/04/27/XDlGjgLKS62tkpB.png" title="填充失败" data-caption="填充失败" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/04/27/XDlGjgLKS62tkpB.png" alt="填充失败"></a><span class="caption">填充失败</span></div><p>可以看到合并单元格没有被正确填充,除了第一个单元格外其他都是 <code>NaN</code> ,而我们期望的是它们都用相同值填充。</p><p>当然我们可以使用 <code>fillna</code> 来实现,不过该方法只能是“具体情况具体分析”,横向、纵向、横纵合并单元格的情况都要根据情况用不同的 fill method,在这里我们至少需要分三种情况来进行处理,显得非常繁琐。一旦变了表格,你的代码就得变,普适性太差。</p><p>按理说,Excel 本身应该保留了合并单元格的信息,比如哪些单元格被合并了,它们的值是什么。应该存在一种工具可以读取出这种信息。</p><p>So,这就是 <code>openpyxl</code> 和 <code>xlrd</code> 派上用场的时候了。</p><h2 id="Solution"><a href="#Solution" class="headerlink" title="Solution"></a>Solution</h2><p>pandas 内部实际上也是用的这两个包。根据<a href="https://pandas.pydata.org/docs/reference/api/pandas.read_excel.html#:~:text=of%20dtype%20conversion.-,engine,-str%2C%20default%20None">官方文档</a>:</p><blockquote><p><strong>enginestr, default None</strong></p><p>If io is not a buffer or path, this must be set to identify io. Supported engines: “xlrd”, “openpyxl”, “odf”, “pyxlsb”. Engine compatibility :<br>• “xlrd” supports old-style Excel files (.xls).<br>• “openpyxl” supports newer Excel file formats.<br>• “odf” supports OpenDocument file formats (.odf, .ods, .odt).<br>• “pyxlsb” supports Binary Excel files.<br><strong><em>Changed in version 1.2.0:</em></strong> The engine <a href="https://xlrd.readthedocs.io/en/latest/">xlrd</a> now only supports old-style <code>.xls</code> files. When <code>engine=None</code>, the following logic will be used to determine the engine:<br>• If <code>path_or_buffer</code> is an OpenDocument format (.odf, .ods, .odt), then <a href="https://pypi.org/project/odfpy/">odf</a> will be used.<br>• Otherwise if <code>path_or_buffer</code> is an xls format, <code>xlrd</code> will be used.<br>• Otherwise if <code>path_or_buffer</code> is in xlsb format, <code>pyxlsb</code> will be used.<br><strong><em>New in version 1.3.0.</em></strong><br>• Otherwise <code>openpyxl</code> will be used.<br><strong><em>Changed in version 1.3.0.</em></strong></p></blockquote><p>简单来说,默认情况下(<code>engine=None</code>):</p><ul><li>如果是 OpenDocument 格式的文件,那么使用 <a href="https://pypi.org/project/odfpy/">odf</a> 解析。</li><li>如果是 xls 格式,那么使用 <code>xlrd</code> 解析。</li><li>如果是 xlsb 格式,那么使用 <code>pyxlsb</code> 解析。</li><li>其他格式都使用 <code>openpyxl</code> 解析。</li></ul><p>原先这些包是可以读取合并单元格这种格式信息的(虽然文档很不完善),但是经过 pandas 后不知道怎么回事就没了。所以这里我们就显式地用这些包来读取和操作。</p><p>总体思路就是:</p><ol><li>用相应的方法读取 Excel 文件,得到 workbook。</li><li>根据 sheet name 取 sheet。</li><li>解析这个 sheet,得到 dataframe。</li><li>获取合并单元格及值和范围。</li><li>根据范围,在 dataframe 中设置相应值。</li></ol><p>完整代码如下:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> pandas <span class="keyword">as</span> pd</span><br><span class="line"><span class="keyword">from</span> openpyxl <span class="keyword">import</span> load_workbook</span><br><span class="line"><span class="keyword">from</span> xlrd <span class="keyword">import</span> open_workbook</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">read_xlsx</span>(<span class="params">file, sheet_name=<span class="literal">None</span>, header=<span class="literal">None</span></span>):</span><br><span class="line"> <span class="string">"""读取 xlsx 格式文件。"""</span></span><br><span class="line"> excel = pd.ExcelFile(load_workbook(file), engine=<span class="string">"openpyxl"</span>)</span><br><span class="line"> sheet_name = sheet_name <span class="keyword">or</span> excel.sheet_names[<span class="number">0</span>]</span><br><span class="line"> sheet = excel.book[sheet_name]</span><br><span class="line"> df = excel.parse(sheet_name, header=header)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> item <span class="keyword">in</span> sheet.merged_cells:</span><br><span class="line"> top_col, top_row, bottom_col, bottom_row = item.bounds</span><br><span class="line"> base_value = item.start_cell.value</span><br><span class="line"> <span class="comment"># 1-based index转为0-based index</span></span><br><span class="line"> top_row -= <span class="number">1</span></span><br><span class="line"> top_col -= <span class="number">1</span></span><br><span class="line"> <span class="comment"># 由于前面的几行被设为了header,所以这里要对坐标进行调整</span></span><br><span class="line"> <span class="keyword">if</span> header <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:</span><br><span class="line"> top_row -= header + <span class="number">1</span></span><br><span class="line"> bottom_row -= header + <span class="number">1</span></span><br><span class="line"> df.iloc[top_row:bottom_row, top_col:bottom_col] = base_value</span><br><span class="line"> <span class="keyword">return</span> df</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">read_xls</span>(<span class="params">file, sheet_name=<span class="literal">None</span>, header=<span class="literal">None</span></span>):</span><br><span class="line"> <span class="string">"""读取 xls 格式文件。"""</span></span><br><span class="line"> excel = pd.ExcelFile(open_workbook(file, formatting_info=<span class="literal">True</span>), engine=<span class="string">"xlrd"</span>)</span><br><span class="line"> sheet_name = sheet_name <span class="keyword">or</span> excel.sheet_names[<span class="number">0</span>]</span><br><span class="line"> sheet = excel.book[sheet_name]</span><br><span class="line"> df = excel.parse(sheet_name, header=header)</span><br><span class="line"></span><br><span class="line"> <span class="comment"># 0-based index</span></span><br><span class="line"> <span class="keyword">for</span> top_row, bottom_row, top_col, bottom_col <span class="keyword">in</span> sheet.merged_cells:</span><br><span class="line"> base_value = sheet.cell_value(top_row, top_col)</span><br><span class="line"> <span class="comment"># 由于前面的几行被设为了header,所以这里要对坐标进行调整</span></span><br><span class="line"> <span class="keyword">if</span> header <span class="keyword">is</span> <span class="keyword">not</span> <span class="literal">None</span>:</span><br><span class="line"> top_row -= header + <span class="number">1</span></span><br><span class="line"> bottom_row -= header + <span class="number">1</span></span><br><span class="line"> df.iloc[top_row:bottom_row, top_col:bottom_col] = base_value</span><br><span class="line"> <span class="keyword">return</span> df</span><br></pre></td></tr></table></figure><p>我们再次用这两个函数读取一下示例文件:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/04/27/FHLSkumy3JQcxhq.png" title="读取 xlsx 格式文件。" data-caption="读取 xlsx 格式文件。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/04/27/FHLSkumy3JQcxhq.png" alt="读取 xlsx 格式文件。"></a><span class="caption">读取 xlsx 格式文件。</span></div><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/04/27/EWL3CU2Pij6VnZb.png" title="读取 xls 格式文件。" data-caption="读取 xls 格式文件。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/04/27/EWL3CU2Pij6VnZb.png" alt="读取 xls 格式文件。"></a><span class="caption">读取 xls 格式文件。</span></div><p>可以看到 xlsx 和 xls 格式文件都能正确读取,同时支持指定 sheet name 和 header。</p><h2 id="需要注意的问题"><a href="#需要注意的问题" class="headerlink" title="需要注意的问题"></a>需要注意的问题</h2><ol><li>如果原先的合并单元格内容为空,那么 <code>openpyxl</code> 的结果会是 <code>None</code> ,而 <code>xlrd</code> 仍然是空字符串。</li><li><code>openpyxl</code> 的 <code>merged_cells</code> 方法似乎在文档中并未出现,忘记了在哪看到的这个方法。</li></ol><h2 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h2><ul><li><a href="https://xlrd.readthedocs.io/en/latest/api.html#xlrd.sheet.Sheet.merged_cells">API Reference — xlrd 2.0.1 documentation</a></li></ul><h2 id="END"><a href="#END" class="headerlink" title="END"></a>END</h2>]]></content>
<summary type="html"><p>使用 pandas 读取带合并单元格的 Excel 的正确方法。[ChatGPT]</p></summary>
<category term="Python" scheme="https://alanlee.fun/tags/Python/"/>
<category term="Data Science" scheme="https://alanlee.fun/tags/Data-Science/"/>
</entry>
<entry>
<title>tqdm+requests:显示下载速度</title>
<link href="https://alanlee.fun/2023/04/09/tqdm-requests/"/>
<id>https://alanlee.fun/2023/04/09/tqdm-requests/</id>
<published>2023-04-09T03:57:00.000Z</published>
<updated>2026-04-14T02:51:14.907Z</updated>
<content type="html"><![CDATA[<!-- excerpt --><!-- toc --><h2 id="Introduction"><a href="#Introduction" class="headerlink" title="Introduction"></a>Introduction</h2><p>在进行大规模数据爬取和下载时,经常需要下载大文件,这时候就需要一个可以显示下载进度和速度的工具,以便于我们能够更好地掌控下载的情况,同时也可以避免下载过程中出现问题导致浪费时间和流量。</p><p>因此,我们需要一个可以显示下载进度和速度的工具,以便于我们更好地掌控下载的情况,避免浪费时间和流量。</p><p>本文将介绍在 Python 中如何使用 tqdm 和 requests 来实现下载进度和速度的显示。</p><h2 id="How"><a href="#How" class="headerlink" title="How"></a>How</h2><p>要使用 tqdm 和 requests 来显示下载进度和速度,我们需要先安装 tqdm 和 requests 模块。安装方法如下(如已安装请跳过):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install tqdm requests</span><br></pre></td></tr></table></figure><p>安装完成后,我们可以使用以下代码来下载文件并显示下载进度和速度:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> pathlib <span class="keyword">import</span> Path</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> requests</span><br><span class="line"><span class="keyword">from</span> tqdm <span class="keyword">import</span> tqdm</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">download</span>(<span class="params">url, folder=<span class="string">"./"</span>, headers=<span class="literal">None</span></span>) -> <span class="built_in">str</span>:</span><br><span class="line"> <span class="string">"""下载文件并显示进度和速度。"""</span></span><br><span class="line"> Path(folder).mkdir(exist_ok=<span class="literal">True</span>, parents=<span class="literal">True</span>)</span><br><span class="line"> local_filepath = Path(folder) / url.split(<span class="string">"/"</span>)[-<span class="number">1</span>]</span><br><span class="line"> headers = headers <span class="keyword">or</span> {</span><br><span class="line"> <span class="string">"user-agent"</span>: <span class="string">"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36"</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">with</span> requests.get(url, stream=<span class="literal">True</span>, headers=headers) <span class="keyword">as</span> r:</span><br><span class="line"> r.raise_for_status()</span><br><span class="line"> size = <span class="built_in">int</span>(r.headers[<span class="string">"Content-Length"</span>])</span><br><span class="line"> chunk_size = <span class="number">8192</span></span><br><span class="line"> <span class="comment"># 重点。</span></span><br><span class="line"> <span class="keyword">with</span> tqdm(</span><br><span class="line"> unit=<span class="string">"B"</span>, <span class="comment"># 1</span></span><br><span class="line"> unit_scale=<span class="literal">True</span>, <span class="comment"># 2</span></span><br><span class="line"> unit_divisor=<span class="number">1024</span>, <span class="comment"># 3</span></span><br><span class="line"> miniters=<span class="number">1</span>, <span class="comment"># 4</span></span><br><span class="line"> desc=<span class="string">f"Downloading <span class="subst">{local_filepath.name}</span>"</span>, <span class="comment"># 5</span></span><br><span class="line"> total=size, <span class="comment"># 6</span></span><br><span class="line"> ) <span class="keyword">as</span> pbar:</span><br><span class="line"> <span class="keyword">with</span> <span class="built_in">open</span>(local_filepath, <span class="string">"wb"</span>) <span class="keyword">as</span> f:</span><br><span class="line"> <span class="keyword">for</span> chunk <span class="keyword">in</span> r.iter_content(chunk_size=chunk_size):</span><br><span class="line"> f.write(chunk)</span><br><span class="line"> pbar.update(<span class="built_in">len</span>(chunk)) <span class="comment"># 7</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> local_filepath</span><br></pre></td></tr></table></figure><p>这段代码的核心是 tqdm,我们使用 tqdm 来创建进度条,然后在循环中更新进度条,实现了下载进度和速度的显示。详细解释如下:</p><ol><li><code>unit="B"</code>:将进度条的单位设置为字节。</li><li><code>unit_scale=True</code>:开启自动缩放功能,根据文件大小自动转换为 KB、MB、GB 等单位,默认是用 SI 公制单位(即 1000 进制),如果想使用二进制单位,则可以指定下面的 <code>unit_divisor</code> 。</li><li><code>unit_divisor=1024</code>:设置单位缩放的除数为1024,即二进制单位。</li><li><code>miniters=1</code>:设置进度条更新的最小步数。对于小文件,进度条可能会更新得太频繁,这个参数可以控制更新频率。</li><li><code>desc="Downloading"</code>:设置进度条的描述。</li><li><code>total=size</code>:设置要下载的文件的总大小,这个值从HTTP响应的 <code>Content-Length</code> 头部获取。</li><li><code>pbar.update(len(chunk))</code>:更新进度条,显示已下载的数据大小。<code>len(chunk)</code>表示当前下载的数据块大小(单位为字节)。</li></ol><p>效果如下:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/04/09/T1VfJ3MNlEBK5Uz.png" title="效果图。" data-caption="效果图。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/04/09/T1VfJ3MNlEBK5Uz.png" alt="效果图。"></a><span class="caption">效果图。</span></div><h2 id="Alternative"><a href="#Alternative" class="headerlink" title="Alternative"></a>Alternative</h2><p>又是设置一堆 tqdm 参数又是需要在循环中增加 update 语句,可能有些人会觉得稍显麻烦,有点侵入性,那我这里有一个不那么麻烦的的近似方法:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">download</span>(<span class="params">url, folder</span>) -> <span class="built_in">str</span>:</span><br><span class="line"> <span class="string">"""一个近似方法。"""</span></span><br><span class="line"> Path(folder).mkdir(exist_ok=<span class="literal">True</span>, parents=<span class="literal">True</span>)</span><br><span class="line"> local_filepath = Path(folder) / url.split(<span class="string">"/"</span>)[-<span class="number">1</span>]</span><br><span class="line"> <span class="keyword">if</span> local_filepath.exists():</span><br><span class="line"> logger.warning(<span class="string">f"<span class="subst">{local_filepath}</span> exists, skip."</span>)</span><br><span class="line"> <span class="keyword">return</span> local_filepath</span><br><span class="line"> <span class="keyword">with</span> requests.get(url, stream=<span class="literal">True</span>, headers=headers) <span class="keyword">as</span> r:</span><br><span class="line"> r.raise_for_status()</span><br><span class="line"> size = <span class="built_in">int</span>(r.headers[<span class="string">"Content-Length"</span>])</span><br><span class="line"> chunk_size = <span class="number">8192</span></span><br><span class="line"> <span class="comment"># 重点。</span></span><br><span class="line"> n_chunks = (size + chunk_size - <span class="number">1</span>) // chunk_size</span><br><span class="line"> <span class="keyword">with</span> <span class="built_in">open</span>(local_filepath, <span class="string">"wb"</span>) <span class="keyword">as</span> f:</span><br><span class="line"> <span class="keyword">for</span> chunk <span class="keyword">in</span> tqdm(</span><br><span class="line"> r.iter_content(chunk_size=chunk_size),</span><br><span class="line"> total=n_chunks, <span class="comment"># 1</span></span><br><span class="line"> unit=<span class="string">"KB"</span>, <span class="comment"># 2</span></span><br><span class="line"> unit_scale=chunk_size / <span class="number">1024</span>, <span class="comment"># 3</span></span><br><span class="line"> ):</span><br><span class="line"> f.write(chunk)</span><br><span class="line"> <span class="keyword">return</span> local_filepath</span><br></pre></td></tr></table></figure><p>这个程序只需要在原来的 <code>r.iter_content()</code> 外像平常一样包一层 tqdm,然后设置较少的参数即可,省掉了一些参数和 update 语句:</p><ol><li><code>total=n_chunks</code>:由于我们是分块下载的,一个很明显的思路是 total 我们只需要设置为总的块数即可,和深度学习中的 batch 类似。</li><li><code>unit="KB"</code>:这里我们门强制让其使用 <code>KB</code> 为单位,而不是自动转换。</li><li><code>unit_scale=chunk_size / 1024</code> :由于 total 设置的是块的数量,默认速度就会显示每秒多少个块,但是我们想让其显示每秒多少 B 或者 KB 之类的,这里我们就用这个参数来让块数 ⨉ 块大小得出来 B 数,然后再除以 1024 的到 KB 数。如果你想得到 MB 数,只需再除以 1024,并设置 <code>unit="MB"</code>即可。</li></ol><p>为什么说这是“近似”做法呢?这是因为我们 <code>unit_scale</code> 直接乘的是块大小,然而每次得到的数据量并不一定是块大小这么大,最后一个块可能会偏小。道理很简单,把 10 按块大小为 3 拆分,最后一个块大小为 1。但是由于我们的块大小不会太大,这个影响微乎其微。</p><h2 id="Thoughts"><a href="#Thoughts" class="headerlink" title="Thoughts"></a>Thoughts</h2><p>进度和速度显示可能是很多程序员忽略的一个问题,但是我觉得非常重要,可以说 you always need ETA(tqdm)。但是在使用的时候要注意刚开始的速度可能并不准确,尤其是任务队列中的处理时间不尽相同时。</p><h2 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h2><ul><li><a href="https://tqdm.github.io/docs/tqdm/#__init__">tqdm.tqdm - tqdm documentation</a></li><li><a href="https://github.com/tqdm/tqdm/blob/master/examples/tqdm_requests.py">tqdm/tqdm_requests.py at master · tqdm/tqdm · GitHub</a></li></ul><h2 id="END"><a href="#END" class="headerlink" title="END"></a>END</h2>]]></content>
<summary type="html"><p>本文介绍了如何使用 Python 中的 tqdm 和 requests 模块来显示下载进度和速度。通过创建进度条并在循环中更新进度条,实现了下载进度和速度的显示。此外,本文还提供了一个近似方法,省略了一些参数和 update 语句。[Notion AI]</p></summary>
<category term="Python" scheme="https://alanlee.fun/tags/Python/"/>
</entry>
<entry>
<title>为什么我的 client CPU 和 server GPU 都很闲?</title>
<link href="https://alanlee.fun/2023/01/13/why-are-my-cpu-client-and-gpu-server-idle/"/>
<id>https://alanlee.fun/2023/01/13/why-are-my-cpu-client-and-gpu-server-idle/</id>
<published>2023-01-13T09:32:00.000Z</published>
<updated>2026-04-14T02:51:14.909Z</updated>
<content type="html"><![CDATA[<!-- excerpt --><!-- toc --><h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>前段时间我在跑数据批处理任务时,发生了一件怪事:client 部署在 CPU 服务器上,server 部署在 GPU 服务器上(模型),client 处理过程中会向 server 发请求。执行了一段时间后通过监控发现,client 这边的 CPU 和 server 这边的 GPU 利用率都不满,client 那边几乎可以忽略不计,server 那边只有 40% 左右。这是为什么?谁在消耗时间?正常情况下 server 应该是满的。</p><h2 id="尝试和解决"><a href="#尝试和解决" class="headerlink" title="尝试和解决"></a>尝试和解决</h2><p>先来大致说下数据批处理流程。流程分为两部分:client 和 server。client 主要负责整个数据处理流程,其中包括向多个模型服务发起请求,不涉及 GPU,部署在 CPU 服务器上。server 就是模型服务,部署在 GPU 服务器上,负责 load 模型和 inference。开始处理的时候,会临时起多台 GPU server,然后上面加一层负载,client 就用这个负载地址来请求 server。</p><p>那天在启动任务开始处理的时候,已经有其他任务在用 server 了,而且是同一个负载地址。我们在启动这个新任务的时候,为了满足计算需求,直接往这个负载下加了 10 台 GPU server。然后启动 client,任务开始。</p><p>启动后 client 的情况:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/01/13/lmQrzfRBYgPK1kx.png" title="启动后的 client htop。" data-caption="启动后的 client htop。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/01/13/lmQrzfRBYgPK1kx.png" alt="启动后的 client htop。"></a><span class="caption">启动后的 client htop。</span></div><p>server 情况:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/01/13/4sflAvSYLXQ9UqK.png" title="启动后的 server GPU 利用率。" data-caption="启动后的 server GPU 利用率。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/01/13/4sflAvSYLXQ9UqK.png" alt="启动后的 server GPU 利用率。"></a><span class="caption">启动后的 server GPU 利用率。</span></div><p>server 这边显示的是所有服务器的 GPU 的情况,包括旧任务的,而且此时旧任务还没有完成。但是新任务一开始基本就出现了利用率的下降,请求量也出现了下滑(但是没有利用率那么明显):</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/01/13/vfqWox6Gac2usNm.png" title="负载请求量。" data-caption="负载请求量。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/01/13/vfqWox6Gac2usNm.png" alt="负载请求量。"></a><span class="caption">负载请求量。</span></div><p>从日志中可以看到旧任务一些进程已经结束了,但是也不至于出现这么大的波动。这就很奇怪了,为什么新任务一开始 server 那边就打不满了呢,按理说应该更满才对,而且请求量也没上去,反而跌了。</p><p>我先是用 <a href="https://github.com/benfred/py-spy">py-spy</a> 检查了其中一个进程到底在干什么,这个工具的一个重要功能就是可以检查处于运行状态下的进程在干什么。结果如下:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/01/13/M7jK2QP1qUGCpVw.png" title="py-spy 结果。" data-caption="py-spy 结果。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/01/13/M7jK2QP1qUGCpVw.png" alt="py-spy 结果。"></a><span class="caption">py-spy 结果。</span></div><p>可以看到,时间基本都消耗在了网络相关函数上,比如 <code>readinto (sockent.py)</code> 、<code>send (requests/sessions.py)</code> 、<code>urlopen (urllib3/connectionpool.py)</code> 、<code>begin (http/client.py)</code> 和 <code>_make_request (urllib3/connectionpool.py)</code> 。很明显,问题出在网络上。正常情况 <code>BeautifulSoup</code> 相关操作应该占比较大一部分。</p><p>运维测试了下网络,延迟很低,网络很通畅……</p><p>然后我在监控中看到 server 镜像版本是旧的,而且这个旧镜像是有问题的,根本启动不起来。从监控中也可以看到这几台 server 利用率一直是 0,也就是说根本没收到请求。如果 server 没起来,那么负载应该是不能连接的,但我在 client 这边并没看到负载连接失败的报错,而且我在 client 程序最前面加了一个负载连接测试,不通过会直接 raise error,程序就会退出,而现在程序是正常运行的。这是为什么呢?还记得前面说过这个负载是和旧任务共用的吗,问题就在这里,这里实际上用的还是旧任务的 server,所以不会出现连接错误。</p><p>把这个问题解决了后,问题依旧,server GPU 利用率稍微降低了些,但波动很小:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/01/13/FrqfEgH4vQ3J9CM.png" title="解决 server 镜像问题后的 GPU 利用率。" data-caption="解决 server 镜像问题后的 GPU 利用率。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/01/13/FrqfEgH4vQ3J9CM.png" alt="解决 server 镜像问题后的 GPU 利用率。"></a><span class="caption">解决 server 镜像问题后的 GPU 利用率。</span></div><p>尽管大家觉得还是很离奇,但是当时时间很晚了,大家建议尝试增加 client 这边的并行核数(joblib 的 n_cores),毕竟 client 这边不满 server 也不满的另一个可能的原因是并发不够,同时打出去的请求不够多。</p><p>后来的小时测试发现速度增加了 5 万/小时,但是不知道这是谁的功劳,或者说这个速度是不是存在水分都不一定,因为我不是特别确定之前的速度。由于时间很晚了,这件事暂且告一段落了。</p><p>后来同事向我反馈线上 api 总是超时,之前不会。我进去看了下日志发现是有个子任务超时了,进一步发现 dev 服务器上的 GPU 特别满,按理说应该不可能,请求的人没那么多。这时我突然想到,为了方便在负载地址和 dev 地址之间切换,我在配置文件里区分了 prd 和 dev,批处理数据时需要先用如下语句来初始化配置:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">config = Config(<span class="string">'prd'</span>) <span class="comment"># or 'dev'</span></span><br></pre></td></tr></table></figure><p>这个语句在两个程序中会出现,我突然想到<strong>我在其中一个程序中似乎还是用的 <code>dev</code> ,有可能会导致批处理用了 dev 服务器上的模型服务</strong>。进去一看,果然!</p><p>这就解释了为什么 client 和 server 都很闲:</p><ul><li>client 很闲是因为 dev 服务器忙不过来,瓶颈在 GPU 这边,即使增加 client 核数也没用;</li><li>server 也不忙是因为一部分请求根本没发到负载。</li></ul><p>但是为什么 server GPU 利用率下降了那么多,只能用旧任务一些进程结束来解释了,当然负载可能也存在问题(后来决定按照任务来生成不同的负载地址,做一下隔离,能避免如服务器没起来但负载仍可以连接的情况,也可以更好地看到不同任务地情况)。</p><p>改成 <code>prd</code> 后 client 这边 CPU 正常了:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2023/01/13/uHivGBITrdQJhW2.png" title="修正后的 client htop 结果。" data-caption="修正后的 client htop 结果。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2023/01/13/uHivGBITrdQJhW2.png" alt="修正后的 client htop 结果。"></a><span class="caption">修正后的 client htop 结果。</span></div><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>经过这件事,意识到几点:</p><ol><li>特别注意配置文件中的配置项是否正确。</li><li>配置尽量初始化一次,然后全局使用。</li><li>Profiling 工具很重要,尤其是能 profile running program 的。</li><li>尝试了很多种方法后仍然不能解决问题,卡住了,先放一放。</li><li>Be patient。</li></ol><h2 id="END"><a href="#END" class="headerlink" title="END"></a>END</h2>]]></content>
<summary type="html"><p>本文分析了一个跑数据批处理任务时,client 和 server GPU 利用率都不满的问题,并对其进行了尝试和解决。最后发现是由于配置文件中的误配置引起的,改正后问题得到解决。[text-davinci-003]</p></summary>
<category term="Python" scheme="https://alanlee.fun/tags/Python/"/>
</entry>
<entry>
<title>NuPhy Air75 矮轴键盘体验</title>
<link href="https://alanlee.fun/2022/12/27/nuphy-air75/"/>
<id>https://alanlee.fun/2022/12/27/nuphy-air75/</id>
<published>2022-12-27T03:32:00.000Z</published>
<updated>2026-04-14T02:51:14.904Z</updated>
<content type="html"><![CDATA[<!-- excerpt --><!-- toc --><blockquote><p>我不是一个键盘专家之类的,所以文中有些表述可能有误,欢迎指正。</p></blockquote><h2 id="开箱"><a href="#开箱" class="headerlink" title="开箱"></a>开箱</h2><p>被这个 <a href="https://nuphy.com/collections/keyboards/products/air75">nuphy air75</a> 种草过很多次,最近又被种草了,结合最近实际及家人的支持(说是新年礼物),就在京东上和键盘皮套一起下单了。第二天就送到了,鉴于当前疫情形势,这么快的速度送到令我惊讶,尤其是和我 20 天前买的东西一起送到……</p><p>到手之后外包装是一个什么食物的箱子,具体什么我忘了,但是上面写着易碎物品,一瞬间我还以为买的其他东西到了。打开之后就是空气袋和键盘、皮套的盒子了。我不是一个二次元爱好者,所以这键盘盒的包装对我来说不是惊喜,而是有点惊吓……</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2022/12/27/tgfJhDn8qo3ZuQy.png" title="键盘盒包装" data-caption="键盘盒包装" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2022/12/27/tgfJhDn8qo3ZuQy.png" alt="键盘盒包装"></a><span class="caption">键盘盒包装</span></div><p>把里面的包装盒抽出来,黑色主体、绿黄橙点缀,看起来挺大气:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2022/12/27/NejKC71F5WLxVGD.png" title="里面的包装盒" data-caption="里面的包装盒" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2022/12/27/NejKC71F5WLxVGD.png" alt="里面的包装盒"></a><span class="caption">里面的包装盒</span></div><p>包装盒里面是绿为主体,和键盘的 ESC 键的绿相呼应,还挺特别。</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2022/12/27/picS4kPK2LzO9qN.png" title="包装盒里面" data-caption="包装盒里面" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2022/12/27/picS4kPK2LzO9qN.png" alt="包装盒里面"></a><span class="caption">包装盒里面</span></div><p>里面的东西如下:</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2022/12/27/z2QA4aHFpPLfUjs.png" title="包装清单" data-caption="包装清单" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2022/12/27/z2QA4aHFpPLfUjs.png" alt="包装清单"></a><span class="caption">包装清单</span></div><p>我目前用到的除了键盘,还有就是键帽拉拔器(<em>键盘默认是 Mac 键帽,需要替换成 Windows 键帽,不过有意思的是,默认模式却是 Win</em>)、磁性脚垫(<em>实际感觉垫高有限</em>)、说明书(<em>各种快捷键真的多</em>),USB 线是用来有线连接、充电时使用,如果使用 <a href="https://nuphy.com/pages/nuphy-console">nupyh console</a>,似乎也必须有线连接,这东西主要是用来重新指派键及自定义灯光。电量显示用右侧测光灯的不同颜色来表示,刚拿到手时我看了下电量(<kbd>Windows</kbd> + <kbd>|</kbd>)还是绿的,表明电量 > 80%,用了一两天变成橙色了(20%~80%)。</p><p>话不多说,来说说优缺点。</p><h2 id="Pros-amp-Cons"><a href="#Pros-amp-Cons" class="headerlink" title="Pros & Cons"></a>Pros & Cons</h2><p>Pros:</p><ol><li><p>颜值高。这是最吸引我的点。我原来用的是 ikbc F87 红轴,当然不是说原来的丑(丑我也不会买啊……),原来的简洁大方,现在的活力多彩,不捧一踩一,只是视觉上习惯了。</p> <div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2022/12/27/VfMoxhtezmPwv5p.jpg" title="ikbc F87 与 NuPhy Air75 对比。Source:自己做的。" data-caption="ikbc F87 与 NuPhy Air75 对比。Source:自己做的。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2022/12/27/VfMoxhtezmPwv5p.jpg" alt="ikbc F87 与 NuPhy Air75 对比。Source:自己做的。"></a><span class="caption">ikbc F87 与 NuPhy Air75 对比。Source:自己做的。</span></div></li><li><p>矮轴。但导致我下定决心换一个的重要因素,就是现在这个是矮轴,就是说键程短。我用 F87 用久了觉得,键程太长了,久了手累,想要普通键盘那种短键程 + 机械键盘的手感。Air75 完美符合,打起来轻松多了,不用再多敲“深”一点了,而且手感也好很多。</p></li><li>声音小。前面说了我原来是红轴,现在这个是茶轴。买之前我在网上查了下区别,说主要是茶轴比红轴有段落感。也听了下声音,感觉上差别不是很大。鉴于我已经用过红轴了,就买了茶轴的。但实际到手用了之后发现,声音差别可太大了,茶轴的明显声音更为沉闷,更小,听起来更舒适更有感觉。</li><li>小,轻。小不用说了,ikbc F87 是 87 键,air75 是 84 键,而且取消了那些不同功能区的间隔。轻很明显,拿起来感觉上轻一半,查了下具体数据,前者重 1100 克,后者 523 克,果然两倍。</li><li>有线、无线、蓝牙三种连接方式。</li><li>双侧侧光灯。由于键盘背光偏弱,白天正常环境亮度下几乎感觉不到背光,但是这两侧的流光灯很亮,但也不至于太过于刺眼,挺有感觉的。除了装饰,主要是用来指示状态,比如大写锁定(左侧)、电量显示(右侧)、连接状态(左侧)等。</li><li>字体粗大舒适。字体明显要比 F87 粗大,尤其是主键盘区,粗得很明显,而且字体也合我口味。</li><li>皮套漂亮……这个属于是败家的,花瓶。</li></ol><p>Cons:</p><ol><li><p>键帽不透光。这就导致很黑的环境中你可能真的就是盲打了,不过这种情况比较少,几乎不会在很黑的环境中用电脑,不然屏幕多刺眼。</p> <div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2022/12/27/Ai9cyTofetnYrMq.png" title="我这是有一点环境光,很黑的环境中字几乎看不到。" data-caption="我这是有一点环境光,很黑的环境中字几乎看不到。" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2022/12/27/Ai9cyTofetnYrMq.png" alt="我这是有一点环境光,很黑的环境中字几乎看不到。"></a><span class="caption">我这是有一点环境光,很黑的环境中字几乎看不到。</span></div></li><li><p>背光偏弱。无论是相比于 F87 还是直观感受,背光确实偏弱,尤其是白天几乎看不到,即使调到最大亮度(从关闭到最亮共 5 档,按 <kbd>Fn</kbd> + <kbd>↑/↓</kbd> 调节)。</p></li><li>没有 <kbd>Insert</kbd> 键。Xshell 默认是 <kbd>Shift</kbd> + <kbd>Insert</kbd> 粘贴(<em>也可以鼠标中键,但是不习惯</em>),但是发现那边被一个 cat 键占据了,这个键是用来唤醒语音助手的,不过基本用不到,可以在 nuphy console 上重新指派为 <kbd>Insert</kbd> 键(<em>目前来说不建议重新指派,见下</em>)。</li></ol><h2 id="重新指派-cat-键为-Insert-键的问题"><a href="#重新指派-cat-键为-Insert-键的问题" class="headerlink" title="重新指派 cat 键为 Insert 键的问题"></a>重新指派 cat 键为 Insert 键的问题</h2><p>默认 cat 键用于唤醒语音助手,不过基本用不到,又缺少一个 Insert 键,所以自然而然会想将其重新指派为 Insert 键。</p><p>但是你重新指派后就会发现,<strong>F 功能键变为纯多媒体键</strong>了,例如 F1/F2 只能用来控制亮度,F2 不再能重命名,失去了原有功能。<strong>下面的操作都解决不了问题</strong>:</p><ol><li>加上 Fn;</li><li>使用 Fn + TAB + F 来切换功能键模式;</li><li>在 console 中恢复出厂设置;</li><li>重启电脑。</li></ol><p>其实这是一个 known issue,可以在 nuphy console v1.0.2 的 <a href="https://nuphy.com/pages/nuphy-console#Nuphy%20Console%20v.1.0.2:~:text=Nuphy%20Console%20v.1.0.2">CHANGELOG</a> 中看到,同时在官方 discord 中也看到了<a href="https://discord.com/channels/864389832829567067/997324960215335023/1052365866261545051">相关回复</a>,说等 v1.0.2 发布就可以了,但没说啥时候发布。</p><p>最终我在 discord 中看到一个人的<a href="https://discord.com/channels/864389832829567067/923144855826358333/1057698469730594816">回复</a>说可以尝试长按 Fn + TAB + R 来 reset。我尝试之后发现确实可以,<strong>解决办法如下</strong>:</p><p>长按 Fn + TAB + R 重置键盘( console 上的恢复出厂设置没用)直到键盘背光灯闪烁,此时双侧侧光灯变蓝,随后左侧侧光灯蓝灯闪烁(表示等待蓝牙连接),此时你的电脑右下角应该就会弹出通知让你连接键盘,连接即可。</p><div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2022/12/30/jU2lIBu4CfFYoEy.png" title="键盘连接通知" data-caption="键盘连接通知" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2022/12/30/jU2lIBu4CfFYoEy.png" alt="键盘连接通知"></a><span class="caption">键盘连接通知</span></div><h2 id="Final-Thoughts"><a href="#Final-Thoughts" class="headerlink" title="Final Thoughts"></a>Final Thoughts</h2><p>总之对这次决定下单购买还是不后悔的,目前用起来很满意,最满意的三点:矮轴,声音,手感。想换键盘的可以试试。不过在 console v1.0.2 发布之前,还是建议不要重新指派 F row key。</p><h2 id="End"><a href="#End" class="headerlink" title="End"></a>End</h2>]]></content>
<summary type="html"><p>这是一篇关于NuPhy air75键盘的开箱体验的文章。作者讲述了他收到这个键盘后的感受,以及里面的包装盒和配件的样子。[ChatGPT]</p></summary>
<category term="Living" scheme="https://alanlee.fun/tags/Living/"/>
</entry>
<entry>
<title>深度学习环境创建指南</title>
<link href="https://alanlee.fun/2022/12/04/install-basic-deep-learning-env/"/>
<id>https://alanlee.fun/2022/12/04/install-basic-deep-learning-env/</id>
<published>2022-12-04T04:15:00.000Z</published>
<updated>2026-04-14T02:51:14.902Z</updated>
<content type="html"><![CDATA[<!-- excerpt --></p><!-- toc --><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>作为机器学习从业者,我们需要一些 package 来辅助我们的工作。很多人也说现在干机器学习都是调包侠,我不是很赞同这种说法,技术越来越进步,进步的意义就在于越来越便捷,越来越 user-friendly,或者说高层,就像汽车一样。</p><p>越来越便捷就可以把宝贵时间留给更有意义的工作,比如数据处理和模型设计。而且可以降低入行门槛,一个行业如果从业者人数太少也不利于行业发展,参考之前传统武术独门绝技啥的传男不传女的规定。另一方面,调包是必要的,但如果你知其然且知其所以然,那更有利于你的工作,特别是 debug 的时候。</p><p>包括在 package 的安装方面,现在也是越来越方便,比刚出来的时候方便多了,像 TensorFlow 的安装都还需要专门写一篇文章来讲,我现在 CSDN 上访问量最高的<a href="https://secsilm.blog.csdn.net/article/details/53418159?spm=1001.2014.3001.5502">文章</a>就是在 Windows 上安装 TensorFlow 的文章,实在是有点意外。</p><p>而本文叫深度学习环境创建指南而不是机器学习环境创建指南,主要是为了强调<strong>深度学习相关工具</strong>的安装。</p><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>为了避免重复劳动,后续可以快速创建环境,以及给有需要的人作参考,本文基于我的工作经历,记录一下 Windows 10 下一个基础深度学习环境的安装,主要包括 PyTorch 和 TensorFlow,其他 package 想起来再加。</p><h3 id="1-创建并激活-conda-环境"><a href="#1-创建并激活-conda-环境" class="headerlink" title="1. 创建并激活 conda 环境"></a>1. 创建并激活 conda 环境</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">conda create -n dl python=3.9</span><br><span class="line">conda activate dl</span><br></pre></td></tr></table></figure><h3 id="2-PyTorch"><a href="#2-PyTorch" class="headerlink" title="2. PyTorch"></a>2. PyTorch</h3><p>pip 方式,要安装哪个 cuda 版本,就相应修改 index url 的 <code>whl/</code> 后面部分即可,但支持哪些需要参考<a href="https://pytorch.org/get-started/locally/">官方文档</a>:</p><div class="alert warning"><p>如果不加后面的 <code>--index-url</code>,那么默认安装的版本要视系统而定,Windows 下是默认安装 CPU 版本,Linux 下是默认安装 GPU 版本。但是强烈建议加,如果不加且你安装的是 GPU 版本,那么可能会安装与你系统 cuda 不匹配的错误的 cuda toolkit 版本,比如你的系统 cuda 是 12.5,但是可能会给你安装 13.0 的 cuda toolkit。</p></div><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118</span><br></pre></td></tr></table></figure><p>conda 方式:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">conda install pytorch torchvision torchaudio pytorch-cuda=11.6 -c pytorch -c nvidia</span><br></pre></td></tr></table></figure><p>测试:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">>>> </span><span class="keyword">import</span> torch</span><br><span class="line"><span class="meta">>>> </span>torch.__version__</span><br><span class="line"><span class="string">'1.13.0'</span></span><br><span class="line"><span class="meta">>>> </span>torch.version.cuda</span><br><span class="line"><span class="string">'11.6'</span></span><br><span class="line"><span class="meta">>>> </span>torch.cuda.is_available()</span><br><span class="line"><span class="literal">True</span></span><br><span class="line"><span class="meta">>>> </span>torch.cuda.device_count()</span><br><span class="line"><span class="number">1</span></span><br><span class="line"><span class="meta">>>> </span>torch.cuda.get_device_name()</span><br><span class="line"><span class="string">'GeForce GTX 1060'</span></span><br></pre></td></tr></table></figure><h3 id="3-TensorFlow"><a href="#3-TensorFlow" class="headerlink" title="3. TensorFlow"></a>3. TensorFlow</h3><div class="alert warning"><p>根据 TensorFlow 官网<a href="https://www.tensorflow.org/install/pip#windows-native">说明</a>,TensorFlow 2.10 是最后一个在原生 Windows 上支持 GPU 的版本。从 2.11 开始,如果你需要在 Windows 上使用 GPU 版 TensorFlow,就必须得在 WSL 中安装了。</p></div><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 我系统上之前似乎已经安装了cuda 11.2,所以这里我就直接安装了。</span></span><br><span class="line"><span class="comment"># 如果你系统上没有cuda,那么可以先用conda安装cudatoolkit,然后再用pip安装TensorFlow;</span></span><br><span class="line"><span class="comment"># 详细参考官方教程(注意切成英文版):https://www.tensorflow.org/install/gpu#windows_setup</span></span><br><span class="line">pip install tensorflow-gpu</span><br></pre></td></tr></table></figure><p>测试:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">>>> </span><span class="keyword">import</span> tensorflow <span class="keyword">as</span> tf</span><br><span class="line"><span class="meta">>>> </span>tf.__version__</span><br><span class="line"><span class="string">'2.10.1'</span></span><br><span class="line"><span class="meta">>>> </span>tf.test.is_built_with_cuda()</span><br><span class="line"><span class="literal">True</span></span><br><span class="line"><span class="meta">>>> </span>tf.test.gpu_device_name()</span><br><span class="line"><span class="number">2022</span>-<span class="number">12</span>-04 <span class="number">11</span>:<span class="number">49</span>:<span class="number">37.252105</span>: I tensorflow/core/platform/cpu_feature_guard.cc:<span class="number">193</span>] This TensorFlow binary <span class="keyword">is</span> optimized <span class="keyword">with</span> oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions <span class="keyword">in</span> performance-critical operations: AVX AVX2</span><br><span class="line">To enable them <span class="keyword">in</span> other operations, rebuild TensorFlow <span class="keyword">with</span> the appropriate compiler flags.</span><br><span class="line"><span class="number">2022</span>-<span class="number">12</span>-04 <span class="number">11</span>:<span class="number">49</span>:<span class="number">38.082555</span>: I tensorflow/core/common_runtime/gpu/gpu_device.cc:<span class="number">1616</span>] Created device /device:GPU:<span class="number">0</span> <span class="keyword">with</span> <span class="number">4632</span> MB memory: -> device: <span class="number">0</span>, name: GeForce GTX <span class="number">1060</span>, pci bus <span class="built_in">id</span>: <span class="number">0000</span>:01:<span class="number">00.0</span>, compute capability: <span class="number">6.1</span></span><br><span class="line"><span class="string">'/device:GPU:0'</span></span><br><span class="line"><span class="meta">>>> </span>tf.sysconfig.get_build_info()</span><br><span class="line">OrderedDict([(<span class="string">'cpu_compiler'</span>,</span><br><span class="line"> <span class="string">'C:/Program Files (x86)/Microsoft Visual Studio/2019/Community/VC/Tools/MSVC/14.29.30133/bin/HostX64/x64/cl.exe'</span>),</span><br><span class="line"> (<span class="string">'cuda_compute_capabilities'</span>,</span><br><span class="line"> [<span class="string">'sm_35'</span>, <span class="string">'sm_50'</span>, <span class="string">'sm_60'</span>, <span class="string">'sm_70'</span>, <span class="string">'sm_75'</span>, <span class="string">'compute_80'</span>]),</span><br><span class="line"> (<span class="string">'cuda_version'</span>, <span class="string">'64_112'</span>),</span><br><span class="line"> (<span class="string">'cudart_dll_name'</span>, <span class="string">'cudart64_112.dll'</span>),</span><br><span class="line"> (<span class="string">'cudnn_dll_name'</span>, <span class="string">'cudnn64_8.dll'</span>),</span><br><span class="line"> (<span class="string">'cudnn_version'</span>, <span class="string">'64_8'</span>),</span><br><span class="line"> (<span class="string">'is_cuda_build'</span>, <span class="literal">True</span>),</span><br><span class="line"> (<span class="string">'is_rocm_build'</span>, <span class="literal">False</span>),</span><br><span class="line"> (<span class="string">'is_tensorrt_build'</span>, <span class="literal">False</span>),</span><br><span class="line"> (<span class="string">'msvcp_dll_names'</span>, <span class="string">'msvcp140.dll,msvcp140_1.dll'</span>),</span><br><span class="line"> (<span class="string">'nvcuda_dll_name'</span>, <span class="string">'nvcuda.dll'</span>)])</span><br></pre></td></tr></table></figure><details> <summary>点击查看如何在 Ubuntu 上安装最新版 tf。</summary><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 对于新版本的 tf,pip install tensorflow 会默认安装支持 GPU 的版本,如果你的环境中没有 GPU 或 cuda,</span></span><br><span class="line"><span class="comment"># 那么会自动使用 CPU。加上 [and-cuda] 后会自动安装 cuda 依赖,再也不用 conda 安装了!</span></span><br><span class="line">pip install tensorflow[and-cuda]</span><br></pre></td></tr></table></figure>测试:<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">>>> </span><span class="keyword">import</span> tensorflow <span class="keyword">as</span> tf</span><br><span class="line"><span class="meta">>>> </span>tf.__version__</span><br><span class="line"><span class="string">'2.17.10</span></span><br><span class="line"><span class="string">>>> tf.test.is_built_with_cuda()</span></span><br><span class="line"><span class="string">True</span></span><br><span class="line"><span class="string">>>> tf.test.gpu_device_name()</span></span><br><span class="line"><span class="string">2022-12-04 11:49:37.252105: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: AVX AVX2</span></span><br><span class="line"><span class="string">To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.</span></span><br><span class="line"><span class="string">2022-12-04 11:49:38.082555: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1616] Created device /device:GPU:0 with 4632 MB memory: -> device: 0, name: GeForce GTX 1060, pci bus id: 0000:01:00.0, compute capability: 6.1</span></span><br><span class="line"><span class="string">'</span>/device:GPU:<span class="number">0</span><span class="string">'</span></span><br><span class="line"><span class="string">>>> tf.sysconfig.get_build_info()</span></span><br><span class="line"><span class="string">OrderedDict([('</span>cpu_compile<span class="string">r', '</span>/usr/lib/llvm-<span class="number">17</span>/<span class="built_in">bin</span>/clang<span class="string">'),</span></span><br><span class="line"><span class="string"> ('</span>cuda_compute_capabilities<span class="string">',</span></span><br><span class="line"><span class="string"> ['</span>sm_60<span class="string">', '</span>sm_70<span class="string">', '</span>sm_80<span class="string">', '</span>sm_89<span class="string">', '</span>compute_90<span class="string">']),</span></span><br><span class="line"><span class="string"> ('</span>cuda_version<span class="string">', '</span><span class="number">12.3</span><span class="string">'),</span></span><br><span class="line"><span class="string"> ('</span>cudnn_version<span class="string">', '</span><span class="number">8</span><span class="string">'),</span></span><br><span class="line"><span class="string"> ('</span>is_cuda_build<span class="string">', True),</span></span><br><span class="line"><span class="string"> ('</span>is_rocm_build<span class="string">', False),</span></span><br><span class="line"><span class="string"> ('</span>is_tensorrt_build<span class="string">', True)])</span></span><br><span class="line"><span class="string">>>> tf.config.list_physical_devices('</span>GP<span class="string">U')</span></span><br><span class="line"><span class="string">[PhysicalDevice(name='</span>/physical_device:GPU:<span class="number">0</span><span class="string">', device_type='</span>GP<span class="string">U')]</span></span><br></pre></td></tr></table></figure></details><h3 id="4-其他杂项"><a href="#4-其他杂项" class="headerlink" title="4. 其他杂项"></a>4. 其他杂项</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install notebook transformers datasets pandas jieba loguru</span><br></pre></td></tr></table></figure><p>最后这个环境已经有 8.67 GB 了……</p><p><img src="https://s2.loli.net/2022/12/04/ndUAombZfLRFIvu.png" alt="dl-env-size"></p><h2 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h2><ul><li><a href="https://pytorch.org/get-started/locally/">Start Locally | PyTorch</a></li><li><a href="https://www.tensorflow.org/install/pip#windows-native">Install TensorFlow with pip</a></li></ul><h2 id="END"><a href="#END" class="headerlink" title="END"></a>END</h2>]]></content>
<summary type="html"><p>创建一个包含 PyTorch 和 TensorFlow 的基础深度学习环境。<br></summary>
<category term="Data Science" scheme="https://alanlee.fun/tags/Data-Science/"/>
<category term="TensorFlow" scheme="https://alanlee.fun/tags/TensorFlow/"/>
<category term="Machine Learning" scheme="https://alanlee.fun/tags/Machine-Learning/"/>
<category term="PyTorch" scheme="https://alanlee.fun/tags/PyTorch/"/>
<category term="Windows" scheme="https://alanlee.fun/tags/Windows/"/>
<category term="sklearn" scheme="https://alanlee.fun/tags/sklearn/"/>
</entry>
<entry>
<title>关于 C4 数据集</title>
<link href="https://alanlee.fun/2022/08/22/about-c4-dataset/"/>
<id>https://alanlee.fun/2022/08/22/about-c4-dataset/</id>
<published>2022-08-22T13:04:19.000Z</published>
<updated>2026-04-14T02:51:14.899Z</updated>
<content type="html"><![CDATA[<!-- excerpt --><p>参考 <a href="https://arxiv.org/abs/2104.08758">Documenting Large Webtext Corpora: A Case Study on the Colossal Clean Crawled Corpus</a>。</p><ol><li>从 365 百万 domain 中抓取,共计大约 1560 亿 token。</li><li>用来训练 T5 和 Switch Transformer。</li><li><a href="https://arxiv.org/abs/1910.10683">Raffel et al. (2020)</a> 提供了重新<a href="https://github.com/google-research/text-to-text-transfer-transformer#c4">创建 C4 的脚本</a>,但是运行这些脚本大概需要数千刀。</li><li>C4 是以 Common Crawl 2019 年 4 月的 snapshot 为基础创建的,使用了很多 filter 来过滤文本。</li><li>这些 filter 的作用包括:<ol><li>删除没有 terminal punctuation mark 的行。</li><li>删除少于 3 个词的行。</li><li>删除少于 5 个句子的文档。</li><li>删除包含包含 <em>Lorem ipsum</em> 这种 placeholder 文本的文档。</li><li>删除包含“<a href="https://github.com/LDNOOBW/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words">List of Dirty, Naughty, Obscene, or Otherwise Bad Words</a>”中任何单词的文档。</li><li>删除非英文文档,非英文的标准是使用 <a href="https://pypi.org/project/langdetect/"><code>langdetect</code></a> 得到的英文概率小于 0.99,所以 C4 主要是英文文档。</li></ol></li><li>应用了 filter 的数据集版本叫 C4.EN,没应用的叫 C4.EN.NOCLEAN,没有使用 blcoklist 的 C4.EN 叫 C4.EN.NOBLOCKLIST。三个版本的简单统计如下图,其中 token 数是用 spacy 的 English tokenizer 分词后统计的:<div class="figure center" style="width:;"><a class="fancybox" href="https://s2.loli.net/2022/09/01/pqYNAy39XhBbSjM.png" title="三个版本的 C4.EN 统计" data-caption="三个版本的 C4.EN 统计" data-fancybox="default"><img class="fig-img" src="https://s2.loli.net/2022/09/01/pqYNAy39XhBbSjM.png" alt="三个版本的 C4.EN 统计"></a><span class="caption">三个版本的 C4.EN 统计</span></div></li><li>来源网址中,按 TLD(top-level domains)统计,前三名是 .com、.org、.co.uk,其中 .gov 和 .mil 占比也不少,后者尽管不在 top25 中,但是也有 33 百万 token。</li><li>按网站统计,前三名是 <code>patents.google.com</code>、<code>en.wikipedia.com</code>、<code>en.m.wikipedia.com</code>。</li><li>按发表时间统计,92% 都发表在数据集收集前的一个十年中(2011-2019),分布是长尾分布 long-tailed,大部分都在数据收集前的 10-20 年间。这是从 C4.EN 中采样得来的,采样大小为 1 百万。发表时间是按照该网址被 Internet Archive 首次索引收录的时间算的,所以真实发表时间实际更早一点。</li><li>按地理位置统计,作者使用了一个 IP-country 数据库,从原始数据集中随机采样了一个大小为 17 万 5 千的样本集。前五名是美国(51.3%)、无法分辨、德国、英国和加拿大。中国排在第 18,香港排在第 16。值得注意的是,按人口算第 2、3、4 大说英语的国家——印度、巴基斯坦、尼日利亚、菲律宾,在数据集中占比只有美国的 3.4%、0.06%、0.03%、0.1%,尽管他们有数千万人说英语。</li><li>C4 包含大量机器生成的文本,machine-generated text,主要包括专利的机器翻译和 ocr 文本。前面说过,按网站统计 <code>patents.google.com</code> 排第一,这是专利网站,Google 会使用机器翻译模型翻译非英文专利,也会使用 ocr 将扫描文本识别出来。识别哪些文本是机器生成的也是一个活跃的研究领域。</li><li>C4 中存在 benchmark data contamination 现象,即下游任务的训练集或测试集出现在 C4 中,造成了数据污染。具体来说,分为两种情况:input-and-label contamination 和 input contamination。</li><li>一些 seq2seq 任务的 label 其实就是 input 中的文本,例如抽取式摘要,如果这种任务的 input 出现在了预训练数据集中,那么其 label 也相当于出现在了预训练数据集中,那么我们有理由认为模型实际上只是在背书而没有做真正的推理。作者分析了 3 个生成式任务的7个数据集,发现均有不同程度(1.87-24.88%)的污染,target 文本为单句的匹配率(完全匹配)要明显高于多句。</li><li>Input contamination 同样会对下游任务造成影响。作者发现有 2-50% 的 GLUE input 出现在 C4 中。对于分类任务来说,虽然不包含 label 的训练集出现在 C4 中并不影响最终性能,但是对 zero-shot 和 few-shot 来说,这仍然是一个值得慎重对待的问题。</li><li>C4 带有明显的种族偏见,“Jewish”更容易与积极情绪挂钩,而“Arab”更容易与消极情绪挂钩。</li><li>对被排掉的文档进行随机抽样,得到 10 万份文档,然后进行 k-means 聚类,k=50,使用 TF-IDF 进行 embedding,然后使用 PCA 进行降维可视化。但最终发现只有 16 个类,且三分之一的是性相关文档。</li><li>相比于种族,提及性取向的文档更有可能被排除,例如 lesbian 和 gay。这个结论是通过计算点互信息 PMI 得到的。</li><li>非裔美国英语 AAE 和西班牙裔美国英语 Hisp 更有可能被排除。</li><li>许多被排除的文档并不包含 offensive 和 sexual 内容。</li><li>97.8% 的 C4.EN 是白人英语 WAE,AAE 和 Hisp 分别只有 0.07% 和 0.09%。</li><li>在创建数据集的过程中,评估 bias 很重要。</li><li>在清洗 web-crawled 数据时,作者反对使用黑名单的方法来排除文档。</li><li>作者分析的是 C4.EN,所以本文结论可能并不适合其他语言。</li><li>GPT-3 的作者在训练完成之后,才发现存在 benchmark contamination。由于重新训练非常昂贵,他们没有重新训练,转而分析不同任务受到该现象的影响,发现确实会影响相关 benchmark 的性能。</li></ol>]]></content>
<summary type="html"><p>C4 数据集速览。</p></summary>
<category term="Paper" scheme="https://alanlee.fun/tags/Paper/"/>
<category term="Dataset" scheme="https://alanlee.fun/tags/Dataset/"/>
</entry>
</feed>