-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
1910 lines (1835 loc) · 221 KB
/
atom.xml
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
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<feed xmlns="http://www.w3.org/2005/Atom">
<title>柏园猫のBlog</title>
<subtitle>「初めて守りたくて 寄り添いこの空のもとで」 </subtitle>
<link href="https://nekomoe.xyz/atom.xml" rel="self"/>
<link href="https://nekomoe.xyz"/>
<updated>2025-01-02T13:08:45.116Z</updated>
<id>https://nekomoe.xyz</id>
<author>
<name>柏园猫のBlog</name>
</author>
<generator uri="https://bbg.nekomoe.xyz">Baiyuanneko's Blog Generator</generator>
<entry>
<title>2024 年终总结</title>
<link href="https://nekomoe.xyz/index.html?type=article&filename=2024-summary.md"/>
<id>https://nekomoe.xyz/index.html?type=article&filename=2024-summary.md</id>
<content type="html">
<![CDATA[
<blockquote>
<p>所以,重要的不是经历,而是选择。
—— 《蔚蓝档案》</p>
</blockquote>
<p>↑上面这句话放在这里没什么深层的特别含义,纯属个人发病(x</p>
<p>我一向认为写年终总结是一个很有意义的活动,因为不去总结的话很多事情就会忘记掉,就比如我在写这篇文章的时候就一直在翻手机相册,试图找出当时的自己做了些什么(</p>
<h2>打了两场 CTF</h2>
<p>今年一共打了两场CTF,一次是线下的比赛,一次是 GeekGame 2024。</p>
<p>这次的线下比赛虽然没有奖金奖品什么的不过能拿到一点成绩对菜菜的我来说已经很满足了,又是被队伍中的大佬带飞的一年啊。比赛的校园很大环境也很好,值得一提的是比赛结束之后主办方竟然还给我们选手准备了晚宴,果然正规比赛就是不一样啊,享受上了。事后发现 MiaoTony 也在现场,虽然当时不知道。</p>
<p>今年 GeekGame 比 HackerGame 早一点所以先打的是 GeekGame,然后艰难地混到了个纪念品和成绩证明。不知道明年自己还有没有机会进校外前30。<del>HackerGame 也打了不过太卷了所以甚至都没什么上榜的机会,眼看进榜无望自己也开摆了(悲</del></p>
<p>另外还参加了 <a href="https://chihuo2104.dev/">chihuo2104</a> 和 <a href="https://soha.moe/">Soha</a> 的新年红包活动。好玩,爱玩!</p>
<h2>蓝桥杯拿了省二</h2>
<p>去年年底报的蓝桥杯 Python 大学 B 组。凭借着在洛谷刷的简单的 DFS 算法混了个省二。还是太菜了(悲)。今年又报了 Java 组,希望下次多多进步,能获得一个更好的成绩吧。</p>
<h2>《恋爱小行星》</h2>
<p>今年看了很多新番和老番,大部分是和 <a href="https://apeiria.net/">Misaka13514</a> 和 <a href="https://koishi514.moe/">scientificworld</a> 一起看的,感谢你们的陪伴。</p>
<p>今年补了一部让我很喜欢的番,就是《恋爱小行星》。“喜欢的事情,无论何时都想要毫无顾虑地去做啊”,这是我最喜欢的里面的一句话。我自己也经常因为很多原因或者小事而自暴自弃地放弃自己明明喜欢做的事情,还是不够坦率啊。比如打比赛的时候,看到自己费了很多精力,好不容易解决了一道题目,然后提交上去,却发现又掉了几名,就会感觉到一种深深的挫败感,然后产生焦虑和想要放弃的念头。还是很羡慕あお(Ao,苍)坦率的性格和愿意为了想要的事情付出的决心。以及女孩子们在一起真是好啊(</p>
<p>另外插播一条新闻:<a href="https://en.wikipedia.org/wiki/697402_Ao">今年九月,一个新发现的小行星以Ao的名字命名了</a>,感动!</p>
<h2>《近月少女的礼仪》</h2>
<p>虽然一开始对于这部作品没有抱有非常大的期待,不过推过之后发现确实是非常让我喜欢的作品。首先是艺术性。就从标题来说,标题“近月少女的礼仪”其实是这样解释的:女主角樱小路露娜代表月亮(露娜→Luna→月),而玩家扮演游戏中靠近女主角的女仆,因此是“近月”。礼仪和规则相对,游戏中有句话是“这并非用来束缚我的规则,而是我应该遵守的礼仪”。这种文字有艺术感而且很耐读,简直是艺术品!其次是玩家扮演的小仓朝日。在剧情中,他(她)对所有人的真诚的爱是很让我感动的,特别是对女主樱小路露娜。虽然现实中很难存在这种完美的性格,不过至少存在于二次元当中。这部还有很多其他<del>奇怪的</del>特点(虽然对一些人来说也许是减分项)包括是少见的全程女仆视点的游戏(<del>就连HS也是,不过HS可能是考虑到很多人不能接受所以设计成可选的了</del>)</p>
<h2>Vue、MongoDB</h2>
<p>今年开始尝试使用一些新技术了,包括 Vue 和 MongoDB,并使用它们开发了几个项目,包括 OpenID (后端使用 Flask + MongoDB)和 <a href="https://ocr.byn.moe/">一个重制版的在线 OCR 工具</a>。本来也并没有这么喜欢这些新技术,但是因为学校教学而被迫学习的 PHP 语言实在令人感到痛苦(恼)</p>
<p>明年打算学习 React。</p>
<h2>游戏</h2>
<p>自己今年玩的比较多的游戏包括《蔚蓝档案》、osu!、Minecraft、《Cyberpunk 2077》、雀魂麻将。</p>
<h2>总结 &amp; 感谢</h2>
<p>虽然还有很多要写的,不过就此停笔罢!</p>
<p>这一年以来很多人给予过我很多温暖和支持,有的是实体上的,有的是精神上的。谢谢你们:</p>
<ul>
<li><a href="https://apeiria.net/">Misaka13514</a>:送我的 ATRI fumo 和《常轨脱离Creative 凸》初回版实体特典,以及陪我玩MC、陪我聊天、一起分享很多很多事情,和你在一起总是很开心!</li>
<li><a href="https://koishi514.moe/">scientificworld</a>:陪我打osu!、陪我聊天、陪我玩MC、和其他很多事情!</li>
<li><a href="https://chihuo2104.dev/">chihuo2104</a>:送我的绮良良周边,陪我聊各种各样的话题和分享各种各样的事情!</li>
<li><a href="https://brightsu.cn/">BrightSu</a>:送我的《少女理论及其周边》实体特典、各种蔚蓝档案周边,以及各种小玩意</li>
<li><a href="https://space.bilibili.com/516252009/">666999HC</a>:送我的吧唧,和对我的支持!</li>
<li><a href="https://blog.mjt.asia/">Muji Togawa</a>:给我制定跑步策略和方案</li>
<li><a href="https://tqlwsl.moe/">wlt233</a></li>
<li><a href="https://mzwing.eu.org/">mzwing</a></li>
<li><a href="https://blog.akuamar1n.com/">zzjzxq33</a></li>
<li><a href="https://github.com/Rikki-Zero">Rikki</a></li>
<li><a href="https://sekaimoe.dpkg123.site/">dabao1955</a></li>
<li><a href="https://github.com/Fisherman110">littlebear</a></li>
<li><a href="https://xlog.mk1.io/">Ray</a></li>
</ul>
<h2>See also</h2>
<ul>
<li><a href="https://koishi514.moe/blog/bbg/index.html?type=article&filename=8YkDYKfG5wDN.md">我的 2024 年终总结 - 無意識の桜</a></li>
<li><a href="https://blog.chihuo2104.dev/posts/goodbye-2024">2024年度总结 - chihuo2104の部落格</a></li>
<li><a href="https://blog.mjt.asia/posts/bc0e860c/">Y2025班次冬日列车 - 反応蒸留</a></li>
<li><a href="https://mzwing.eu.org/index.html?type=article&filename=wave-to-my-future-self.md">碎记•向明年的自己招个手 - 洛仙璃の幻梦</a></li>
<li><a href="https://blog.akuamar1n.com/2024/12/31/2024-summary/">2024年终总结 - zzjzxq33&#39;s blog</a></li>
<li><a href="https://sekaimoe.dpkg123.site/posts/end-of-2024/">二葉琉璃的 2024 - SekaiMoe&#39;s World</a></li>
<li><a href="https://blog.awaae001.top/posts/57024.html">再见2024 · 共赴一场烟火绚烂 - 呓语梦轩</a></li>
<li><a href="https://xlog.mk1.io/goodbye-2024">辞旧迎新:Ray 的 2024 年终总结 - Ray&#39;s xLog</a></li>
<li><a href="https://www.mole9630.top/year-end-2024/">年终总结2024 - 摩尔の镇 | モル·町</a></li>
<li><a href="https://world.ccrice.com/2024/12/25/2024%E5%B9%B4%E5%BA%A6%E6%80%BB%E7%BB%93/">2024年度总结 - CC米饭的小世界</a></li>
</ul>
]]>
</content>
<summary type="html">我的 2024</summary>
<updated>2025-01-02T03:39:38.411Z</updated>
<published>2025-01-02T03:39:38.411Z</published>
</entry><entry>
<title>再次喜欢上 Surface Go 吧!</title>
<link href="https://nekomoe.xyz/index.html?type=article&filename=surface-go-linux.md"/>
<id>https://nekomoe.xyz/index.html?type=article&filename=surface-go-linux.md</id>
<content type="html">
<![CDATA[
<p>虽然已经忘记了自己第一次使用平板电脑是什么时候,不过我还记得自己第一次拥有属于自己的平板电脑是一台 iPad mini 2. 那是在我小学的时候,我父母买给我的。这也几乎是我第一次接触苹果设备。再之后,应该是初中的时候,因为 iPad mini 的电池寿命已尽,我父母给我换了 iPad(不过现在已经忘记是第几代型号了)。那时候我折腾过些什么呢?越狱嘛,虽然尝试过不过失败了,毕竟当时欠缺相关知识而且技术力不够。对当时的印象只有陪伴我折腾苹果设备的爱思助手/iTools这种第三方工具了。不过令人羞愧的是我用爱思助手最大的作用只用来免费下载盗版软件。而且,假如让我重生到初中重新开始使用苹果设备,现在的我也不屑于把这种第三方的商业软件安装到自己的设备上了。</p>
<p>iPad 的体验当然没得说:相比 Android 平板显著更好的生态、统一的体验、性能尚可的配置,这些都是 iPad 相比别家的优势。不过我是从没觉得做果粉有意思过。虽然现在我什么粉都不是,但如果让我在做果粉和软粉中选择一项,我大概会选择后者。觉得微软的生态有意思对我来说大抵是从初中开始的。彼时微软刚刚推出 Windows 8,当微软的 Windows 8 把科技感满满的 Metro 磁贴设计广而告之的时候,这个设计风格对于一个当时沉浸在 Win7 Aero 效果中的小孩哥(其实就是我)的震撼是无以言表的。换句话说这也可以说是给我对软件 UI 设计和扁平化风格的启蒙吧。</p>
<p>从那时起我便梦想着有一台 Surface——我记得当时微软商城官网挂着的是经典的 Surface Pro 3,宣传视频和贴吧里吧友的使用体验看得我眼馋不已,但是毕竟父母已经给我买了 iPad,因此我是无缘体验到了。时光飞速流转,我已经步入大学两年了,经过各种机缘巧合,我在 <a href="https://chihuo2104.dev">chihuo2104</a> 的推荐下收了一台成色尚可的 Surface Go 一代。虽然现在的我已不是软粉,也不再执着于折腾各种系统了。</p>
<blockquote>
<p>当我在寝室里打开了闲鱼收的二手 Surface Go 时,脑海中总会想起 10 年前第一次给电脑安装 Windows 8 的那个遥远的下午.</p>
</blockquote>
<h2>Overview</h2>
<p><img src="./images/surface-go.jpg" alt="image"></p>
<h2>二合一合一合一虎视眈眈</h2>
<p>非常幸运的是,我的这台 Surface Go 并不是 4GB 的低配版,而是配备了 8GB 内存和键盘的高性能版本。卖家甚至贴心地送了一个 SD 卡。有了键盘,意味着这是一个二合一设备。Surface Go 的键盘采用磁吸式设计,不用时可以取下键盘,使用的时候再安上。如果取下了键盘,或者把键盘放到了后盖上,则自动进入平板模式,默认可以调出屏幕键盘。尽管 Win11/Win10 的工作重点并不在平板适配上,但是仍然保留了这些方便的功能。值得一提的是,作为运行着 Windows 的设备,生态主要以 Win32 应用程序为主。为触屏优化过的程序并不是很多,大多数在触屏环境下只能将就使用甚至艰难使用,甚至包括微软自家的一些应用(例如在平板模式下 VS Code 会频繁触发屏幕键盘,难以正常使用)。当然,UWP 应用的触屏体验确实良好,但目前 UWP 应用很多也已不再更新,换句话说,由于 Windows 应用生态对触屏的支持并不良好,像 iPad 那样买前生产力、买后爱奇艺的现象并不一定会出现——买前爱奇艺、买后生产力还差不多。</p>
<h2>熙熙攘攘我们的云游戏</h2>
<p>作为一台配置在今天来看稍显落后的设备,它只能用来玩玩 Galgame 吗?事实上,我们甚至可以玩原神!当然,是使用云游戏串流方案。</p>
<p>我尝试过的云游戏平台主要有米哈游云原神和网易云游戏。假如你尝试从它们的官网下载客户端然后运行,你一定会发出“我们的应用生态确有问题!”的感叹。没错,因为上面提到过的原因,这些客户端根本不支持触屏操作。怎么解决呢?如果你看过 B 站上那些在各种奇妙的地方(比如作业帮应用)里打开原神的视频,你会注意到它们都是利用云原神网页版进行的,而且完全是可玩的。这是因为云原神网页版对 Android UA 做了触屏适配,但客户端没有。网易云游戏也是差不多的情况。所以答案就是去浏览器里找个改 UA 的插件,然后用 Android UA 访问网页版的云原神/网易云游戏即可使用触屏享受云游戏了。</p>
<h2>Windows is All you need...?</h2>
<p>Surface Go 预装的是 Windows 10 系统。到手之后,我迫不及待的安装了 Windows 11,并安装了各种应用,然后收获了一个无比卡顿的系统。</p>
<p>卡顿指的是以下几点:(1)应用打开时间长。由于 CPU 性能孱弱和 Win11 对于老设备糟糕的优化,打开软件可能需要耗费很长的时间,即使 Surface Go 配备的是 SSD.(2)开机时间长。许多应用喜欢设置开机启动项,如果你忘记取消它们,再与第一点的原因叠加起来,开机启动可能会耗费非常长的时间。(3)应用切换会有明显迟滞感,尤其是当内存占用非常大的时候。比如你同时在使用浏览器和文件管理器和一些其他应用,从任何一个切换到另一个都会有卡顿的感觉。</p>
<p>当然 Surface Go 是老设备,我不是故意安装 Win11 的,求求你回到 Win10 好不好,我什么都愿意做的!</p>
<h2>The Happy Path</h2>
<p>我并不想装回 Win10,但我也不能再忍受 Win11 的卡顿了。于是从没给电脑以外的设备安装过 Linux 的我在前几天开始了尝试。</p>
<p>我尝试的发行版是 Kubuntu 24.10。怎么安装呢?答案是:和给电脑安装 Linux 一样的安装。Surface Go 虽然看上去是一台平板电脑,但它其实更像一台笔记本电脑。它可以非常方便的进入 BIOS 设定页面和修改启动项,也可以关闭安全启动然后调整为从 U 盘启动,剩下的步骤相信都不用说了。值得一提的是,Surface Go 只有一个 Type C 的口,如果你打算在 U 盘中烧录安装镜像的话,需要提前准备好转接头。</p>
<p>关于 Surface 如何进入 BIOS 并修改启动项,可以参考这篇文章:<a href="https://support.microsoft.com/en-us/surface/how-to-use-surface-uefi-df2c8942-dfa0-859d-4394-95f45eb1c3f9">https://support.microsoft.com/en-us/surface/how-to-use-surface-uefi-df2c8942-dfa0-859d-4394-95f45eb1c3f9</a></p>
<h3>更换到 Surface Kernel</h3>
<p>在完成发行版的安装之后,可以考虑更换到 Surface Kernel.</p>
<p>GitHub 上有一个项目 <a href="https://github.com/linux-surface/linux-surface">linux-surface/linux-surface</a>,其中提供了适用于 Surface 的 Linux Kernel。即使不用这个 Kernel,基本功能仍然可以正常使用。不过这个 Kernel 提供了对于一些驱动的更好支持,例如摄像头。</p>
<p>可以参考这个教程安装 Surface Kernel:<a href="https://github.com/linux-surface/linux-surface/wiki/Installation-and-Setup">https://github.com/linux-surface/linux-surface/wiki/Installation-and-Setup</a></p>
<h3>摄像头支持</h3>
<p>安装完成后,你可能会发现摄像头不能正常使用。这时可以参考这篇教程:<a href="https://github.com/linux-surface/linux-surface/wiki/Camera-Support">https://github.com/linux-surface/linux-surface/wiki/Camera-Support</a></p>
<h3>触摸屏、屏幕键盘与图形系统</h3>
<p>虽然我一直主张应该尽量使用 Wayland,不过目前我并没有成功在 KDE Wayland 下成功处理好屏幕键盘的支持。我尝试了 Maliit Keyboard,但是并没有找到成功调出这个键盘的方法,Fcitx 5 似乎有一些 Virtual Keyboard UI,但是都需要编译安装使用。</p>
<p>目前我使用的是 X11 图形系统配合 OnBoard 实现屏幕键盘。在任何时候,OnBoard 的屏幕键盘都不会自动弹出,你需要手动点击托盘图标或任务栏快捷方式进行运行。</p>
<p>Firefox 在 X11 下默认不支持触摸屏。可以参考 <a href="https://superuser.com/questions/1151161/enable-touch-scrolling-in-firefox">https://superuser.com/questions/1151161/enable-touch-scrolling-in-firefox</a> 这篇教程解决。</p>
<h3>运行 Android 应用和游戏</h3>
<p>目前 Linux 下流行的安卓模拟器有 Genymotion、Waydroid 和 xDroid. 我推荐使用 Waydroid.</p>
<p>Waydroid 只占用一点点内存,而且性能开销并不大,并且有良好的触屏支持。安装一个 Android 模拟器也可以极大地改善应用生态问题。</p>
<p>如果你正在使用 Wayland 图形系统,直接参考官方教程安装即可:<a href="https://docs.waydro.id/usage/install-on-desktops">https://docs.waydro.id/usage/install-on-desktops</a></p>
<p>如果你像我一样正在使用 X11 图形系统,那么就需要一些其他的小技巧了。关键在于我们如何在 X11 下模拟 Wayland 的环境。<del>由于 Wayland 下的 X11 兼容层被称作 xwayland,根据乘法交换律可知 X11 下的 Wayland 兼容层叫做 waylandx...</del>才怪!我们应该怎样模拟 Wayland 环境呢?方法有几种。(1)使用 cage. 我们可以安装软件包 <code>cage</code> 然后执行 <code>cage -- waydroid show-full-ui</code>.(2)使用 <code>kwin-wayland</code>. 我们可以执行 <code>kwin_wayland &quot;waydroid show-full-ui&quot;</code>。(3)使用 Weston. 我们需要先安装 Weston,然后在终端中执行 <code>weston</code>。这时会在单独的窗口中打开 Weston 混成器。这个窗口内容的左上角有一个终端的图标,点击一下即可打开 Weston Terminal. 在 Weston Terminal 中执行 <code>waydroid show-full-ui</code> 即可。</p>
<p>我个人使用的是第三种方法。如果 Waydroid 的界面和字体看上去有点小,进入 Waydroid 的系统设置中的 Display and brightness 中有一个 Display Size,向上调高两格即可。</p>
<p>为了方便没有用过 Waydroid 的人,这里简单介绍一些使用技巧:</p>
<p>首先 Waydroid 是可以安装 Google Play 框架和商店的。如果在一开始的初始化阶段选择了 VANILLA 镜像也没事,后期还可以通过 waydroid_script 手动安装。</p>
<p>然后需要记住几条重要的 Waydroid 命令。<code>waydroid session start</code>和<code>waydroid session stop</code>是用来打开和结束当前会话的。比如如果你需要重启 Waydroid,就可以先 stop 再 start. 还有一条是 <code>waydroid show-full-ui</code> 是用来显示 Waydroid 实际窗口的。</p>
<p>然后就是用 Waydroid 几乎必备的 waydroid_script 了。它的地址在这里:<a href="https://github.com/casualsnek/waydroid_script">https://github.com/casualsnek/waydroid_script</a>. 它有非常多的功能,包括安装 Google Play 框架和商店啊,还有安装 ARM 兼容层之类的。</p>
<p>Waydroid 不自带 ARM 兼容层。因此,在运行一些程序时,会遇到困难。这时候需要安装一个 ARM 兼容层。这个有两种选择,一是 libndk,二是 libhoudini. 我使用的是 libhoudini. 因为我玩 Blue Archive. BA 这个游戏的特点就是用 libndk 运行会报游戏资源错误,而使用 libhoudini 又会在启动页面闪退。幸运的是,libhoudini 会使游戏在启动页面闪退的问题是有 patch 可以解决的,但 libndk 不能。</p>
<p>首先使用 waydroid_script 安装 libhoudini,然后下载这个 patch 并应用即可:<a href="https://github.com/waydroid/waydroid/issues/788#issuecomment-2162386712">https://github.com/waydroid/waydroid/issues/788#issuecomment-2162386712</a></p>
<p><img src="./images/surface-go-ba.jpg" alt="image"></p>
<h3>Hardware-accelerated video decode</h3>
<p>视频硬解应该可以 work out of box. 可以安装 <code>intel-gpu-tools</code> 然后运行 <code>sudo intel_gpu_top</code> 观察这一点。</p>
<p><img src="./images/surface-go-video-decode.jpg" alt="image"></p>
<h2>再次喜欢上 Surface Go 吧!</h2>
<p>Surface Go 上的 Linux 体验应该说是不错的,特别是响应速度和卡顿问题相对来说进行了比较好的解决。曾经我由于卡顿而吃灰的 Surface Go 又重新焕发了生机!</p>
<p>标题 neta 自游戏《ISLAND》的章节小标题之一,没有什么特别的含义(</p>
<p><img src="./images/surface-go-fastfetch.jpg" alt="image"></p>
]]>
</content>
<summary type="html">简单折腾了一番 Surface Go 而写出的小笔记</summary>
<updated>2024-11-11T09:13:26.793Z</updated>
<published>2024-11-11T09:13:26.793Z</published>
</entry><entry>
<title>将 Flask 部署到生产环境</title>
<link href="https://nekomoe.xyz/index.html?type=article&filename=deploy-flask-to-production.md"/>
<id>https://nekomoe.xyz/index.html?type=article&filename=deploy-flask-to-production.md</id>
<content type="html">
<![CDATA[
<p><img src="./images/Azusa20240902.png" alt="图文无关,来源:Blue Archive"></p>
<p>(↑ 图文无关,图片来源:Blue Archive / 白洲梓)</p>
<h2>前言</h2>
<p>Flask 应该还算是一个比较常用的 Web 框架,我此前也是用 Flask 开发过几个小项目,不过,怎么将 Flask 部署到生产环境中呢?</p>
<p>如果对于 Web 服务器的部署流程稍有了解的话,就应该知道我们肯定不能直接在目标服务器上执行一个 <code>pip install flask</code> 然后 <code>flask --app server run</code> 这样操作——毕竟我们开发的时候也是用的这条命令,然后在生产环境也是用的这条命令,考虑到生产环境和开发环境的差异还是比较大的,这不对吧(</p>
<p>的确不对,事实上,当我们在开发环境无数次地运行这条命令的时候,都会注意到上面有一行醒目的红字:</p>
<pre><code>WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead
</code></pre>
<p>这其实已经给我们指明了方向,也就是说我们需要使用一个可用于生产环境的比较正式的 WSGI 服务器软件来在生产环境运行我们的 Flask 应用。如果要细究为什么一定要用 WSGI 服务器而不是直接用 Flask 的话,我总结了以下几点原因:</p>
<ul>
<li>在默认的配置下,Flask 一次只能处理一个请求,所以显而易见地性能不好</li>
<li>Flask 本身设计是在开发环境使用的,并不是为了在生产环境下稳定、安全、可靠的运行而设计的,如果要在生产环境使用,还是要用一个正规一点的 WSGI 服务器</li>
<li>而且,Flask 支持 Debugger 这些功能,通过 Debugger 可以实现任意命令执行,万一要是在生产环境不慎启用了,后果不堪设想!</li>
</ul>
<h2>WSGI 服务器的选择和安装</h2>
<p>WSGI,全称是 Python Web Server Gateway Interface,大体上就是将我们的 Python 环境和 Web 服务器进行通信的一种接口。我个人是把它理解为和 CGI 近似的东西,或者说是 CGI 的 Python 版本(我乱说的不一定对,只是我个人这样理解的)。不过这个其实不是重点,重点是我们要怎么去找到这个可以在生产环境使用的 WSGI 服务器软件。我们可以直接参考 <a href="https://flask.palletsprojects.com/en/3.0.x/deploying/">Flask 的官方文档</a>:上面直接列出了几个比较流行的 WSGI 服务器软件,包括 Gunicorn 和 Waitress。</p>
<p>Gunicorn 官网:<a href="https://gunicorn.org/">https://gunicorn.org/</a></p>
<p>Waitress 官网:<a href="https://docs.pylonsproject.org/projects/waitress/en/stable/index.html">https://docs.pylonsproject.org/projects/waitress/en/stable/index.html</a></p>
<p>那么 Gunicorn 和 Waitress 应该选哪个呢?我个人认为比较明显的是平台支持上的差异:</p>
<ul>
<li>Gunicorn 不支持 Windows,不过在 WSL 上也能运行</li>
<li>相比之下 Waitress 支持包括 Windows、Linux 在内的一些平台,所以兼容性更加广一点</li>
</ul>
<p>除此之外,这两个我都使用过了,在我的场景下,我没有感受到明显的差异。我最终是选择了 Waitress,除了兼容性的因素之外,如果它名字叫 Maid 就更好了(不是)</p>
<p>安装和使用方法:</p>
<pre><code>pip install waitress
waitress-serve --host 127.0.0.1 --port 8000 server:app
</code></pre>
<p>这里你可以注意到,我所监听的是 127.0.0.1,而不是 0.0.0.0 或者某个网站域名,这并不是意味着我学艺不精,不知道应该使用 0.0.0.0 或者指定一个域名才能监听来自公网的流量,而是因为我们接下来还需要设置一个至关重要的东西:反向代理。我们打算,通过反向代理监听某个域名,然后把请求转发给本地 127.0.0.1 的 Waitress 服务器。这并不是我故意想多找点事做,而是因为我们需要区分清楚应用服务器和 Web 服务器之间的差异。包括在 Waitress 的官方文档<ref url="https://docs.pylonsproject.org/projects/waitress/en/latest/reverse-proxy.html">Using Behind a Reverse Proxy - Waitress 官方文档</ref>中也有说:</p>
<blockquote>
<p>Often people will set up &quot;pure Python&quot; web servers behind reverse proxies, especially if they need TLS support (Waitress does not natively support TLS). Even if you don&#39;t need TLS support, it&#39;s not uncommon to see Waitress and other pure-Python web servers set up to only handle requests behind a reverse proxy; these proxies often have lots of useful deployment knobs.</p>
</blockquote>
<p>翻译一下:</p>
<blockquote>
<p>人们经常会在“纯 Python”网络服务器的后面设置一个反向代理,尤其是当他们需要 TLS 支持时(Waitress 本身不支持 TLS)。即使你不需要 TLS 支持,也常常会看到 Waitress 和其他纯 Python 网络服务器被设置为仅在反向代理后处理请求;这些代理通常有很多有用的部署选项。</p>
</blockquote>
<p>简单来说,使用反向代理之后,允许我们在反向代理这一层进行很多更加高级和上层的配置,比如说这里提到的 SSL 支持,还有一些别的,比如 IP 过滤之类的。当然,这些功能不是说非要用反向代理否则就不能实现,比如说我们可以在 Flask 应用里面写一些全局的 IP 过滤器,可能也能实现过滤 IP 的效果。不过这个涉及到一个东西放在哪里更加合适的问题。通常来说,我们认为设置一个额外的反向代理服务器来实现这些功能,包括 SSL 支持等,是更加合适的。</p>
<h2>选择和安装反向代理服务器</h2>
<p>最常见的选项是 Nginx,当然还有些别的,比如 Apache 和 Caddy. 我是一个 Caddy 厨,因为 Caddy <em>非常</em> 方便。</p>
<ul>
<li>使用 Caddy 启动的任何网站,它会自动帮你申请和续期 SSL 证书。不再需要配置 <code>acme.sh</code> 或者其它类似的任何东西,只要你的网站里指定了域名信息,它就会全自动地进行证书的获取和续期。</li>
<li>Caddy 对于任何常见需求的写法都 <em>非常</em> 简洁。简单举个例子:如果我们需要启动一个静态文件服务器,需要几行代码呢?答案是只需要一行。</li>
</ul>
<pre><code>file_server
</code></pre>
<p>同样的,如果我们需要进行反向代理,需要几行代码呢,也是一行:</p>
<pre><code>reverse_proxy localhost:8000
</code></pre>
<p>简而言之,使用 Caddy 可以让你省下配置 SSL 和研究配置文件写法的一大笔时间。当然这个东西也并非完美无缺,首先,复杂的配置肯定还是需要查文档的,当然文档本身读起来也不费劲,不过如果你要用 ChatGPT 之类的生成 Caddyfile 配置文件的话,你可能会失望了。这可能是因为 Caddy 属于较新的服务器软件,所以社区资料相比 Apache 和 Nginx 确实属于偏少的状态。当然你让 GPT 生成一段配置文件给你肯定能生成,但是能用吗,如能。大部分情况下你还是需要根据报错自己排查,查看文档,进行合适的修改的。因此,如果你比较依赖使用 GPT 进行配置生成和错误排查,可能 Nginx 和 Apache 会更适合你。</p>
<p>不管怎样,我这里是使用 Caddy 作为反向代理服务器。安装 Caddy 的方法不再说了,这里直接写怎么写配置文件。</p>
<p>默认的配置文件是在 <code>/etc/caddy/Caddyfile</code>,使用 <code>nano</code> 编辑它。</p>
<p>我们假设我们要部署到的域名是在 <code>example.com</code> 这里,并假设我们的 Waitress 是监听在 <code>127.0.0.1:8000</code>。那么我们的 Caddyfile 应该这么写:</p>
<pre><code>example.com {
reverse_proxy localhost:8000
}
</code></pre>
<p>使用<code>systemctl</code>重启一下<code>caddy</code>服务:</p>
<pre><code>systemctl restart caddy
</code></pre>
<p>然后修改 DNS,你会发现 Caddy 自动给你的域名申请并使用了一个 SSL 证书。再次证明了使用 Caddy 可以为我们的服务部署提供很多便利。当然这不是重点。在现在这样的情况下,你的网站(在理论上)应该已经可以正常对外提供服务了。你可以试试访问网站的域名,看一看各种请求是不是都能够正常发送。</p>
<p>如果你像我一样使用 Cloudflare 对流量进行了代理,可能会发现自己的服务器怎么遇到了一些奇怪的问题(比如访问超时,或者无限 308 跳转之类),这时候,我们需要做一些小小的调整。</p>
<h2>与 Cloudflare 相互配合</h2>
<h3>SSL/TLS 模式的正确选择</h3>
<p>首先,我们需要正视 Cloudflare 的 SSL/TLS 模式选择。这个东西绝对不是选择什么都无所谓,在不熟悉的情况下,我们对它的理解并不一定正确。比如,我一开始没有注意这个模式选择,直接采用了它的默认设置 <code>Flexible</code> (弹性, 灵活),从而造成了一个困扰了我很久才修好的问题:无限 308 跳转。</p>
<p>这里贴出一些这种错误的症状:</p>
<ul>
<li>preflight is invalid (redirect)</li>
<li>redirect is not allowed for preflight request</li>
<li>(网站)将你重定向的次数过多,尝试清除 Cookies 后再试</li>
<li>Too many redirects</li>
<li>Error: Exceeded maxRedirects. Probably stuck in a redirect loop</li>
</ul>
<p>TLDR:<strong>如果你遇到了这种问题,那么很有可能是因为你的 Cloudflare 的 SSL/TLS 配置选择了默认的 Flexible,不妨试一试改成 Full 或 Full (Strict),可能就好了。</strong> 实际上,只要你的服务器正确配置了 SSL 证书,永远最正确的选择就是使用最严格的 Full (Strict) 模式。</p>
<p>实际上,这是一个 Cloudflare 的 SSL/TLS 模式非常容易遇到的一个问题(指无限308),并且 Cloudflare 也有一个专门的 TroubleShooting 页面来说明这个问题:<a href="https://developers.cloudflare.com/ssl/troubleshooting/too-many-redirects/">https://developers.cloudflare.com/ssl/troubleshooting/too-many-redirects/</a></p>
<p>感谢 <a href="https://koishi514.moe">scientificworld</a> 和 <a href="https://apeiria.net">Misaka13514</a> 给我建议和帮我调试这个问题。</p>
<p>简单来说,我们可以查阅一下 Cloudflare 官方文档,探究一下 Flexible 和 Full / Full (Strict) 都有什么区别。</p>
<p>首先是 Flexible:</p>
<blockquote>
<h3>Flexible encryption mode</h3>
<p>If your domain’s encryption mode is set to Flexible, Cloudflare sends unencrypted requests to your origin server over HTTP.</p>
<p>Redirect loops will occur if your origin server automatically redirects all HTTP requests to HTTPS.</p>
</blockquote>
<p>注意到 Flexible 模式下,Cloudflare 与源服务器通讯时使用的是 HTTP 流量而非 HTTPS.</p>
<p>然后是 Full 和 Full (Strict):</p>
<blockquote>
<h3>Full or Full (strict) encryption mode</h3>
<p>If your domain’s encryption mode is set to Full or Full (strict), Cloudflare sends encrypted requests to your origin server over HTTPS.</p>
<p>Redirect loops will occur if your origin server automatically redirects all HTTPS requests to HTTP.</p>
</blockquote>
<p>而相比之下 Full / Full (strict) 模式下,Cloudflare 与源服务器通讯时是使用的 HTTPS 协议进行通讯。</p>
<p>部分服务器软件环境下,比如在我的配置环境下,我的服务器会默认将所有的 HTTP 请求 308 到 HTTPS 上,如果我们没有套 Cloudflare CDN,那么客户端直接就是我们的电脑,这时候服务器返回的 308 理应是可以正常奏效的(也就是正确地让我们跳转到 HTTPS 而不是 HTTP),然而我们套了一层 Cloudflare CDN 之后我们客户端这里实际上始终都是 HTTPS 的,需要正确跳转到 HTTPS 的 Cloudflare 与我们服务器的通讯而不是我们与服务器的通讯。当然服务器发来的 308 请求并不会对此施加正确的影响,在这一点上, Cloudflare 的 Flexible 模式下始终是与我们的服务器使用 HTTP 通讯,不管服务器怎样要求 308 跳转,Cloudflare 与服务器这一段的通讯方式都不会改变(始终是 HTTP),也因此,相当于一直在与服务器进行 HTTP 的通讯,而服务器也一直指示希望采用 HTTPS 方式通讯,造成了无限的 308 跳转。解决方式也很简单,就是我们将 SSL/TLS 模式替换为 Full 或 Full (strict),这样就强制 Cloudflare 在和我们的源服务器通讯的时候必须使用 HTTPS 协议。</p>
<h3>获取客户端真实 IP</h3>
<p>当我们使用反向代理的时候,由于中间多了一层跳转,所以假使我们 Flask 应用中写了类似 <code>request.remote_addr</code> 这样的代码,并不能正确获取到对端的 IP。对此,通常的解决办法是让反向代理服务器给应用程序通过其它方式(比如 HTTP 请求头)传输客户端的真实 IP。当然,这时候我们必须修改应用程序里面所有用到 <code>request.remote_addr</code> 的代码,这个是不可避免的。</p>
<p>这里我们采用的解决方案是让反向代理服务器(在我们的情况下是 Caddy)给 Waitress 设置一个额外的 HTTP Header:<code>X-Real-IP</code>,然后修改我们 Flask 应用中所有与 <code>request.remote_addr</code> 相关的代码,将相关逻辑修改为读取我们手动设置的 <code>X-Real-IP</code> 头。</p>
<p>首先是在 Caddy 这一侧,添加设置 <code>X-Real-IP</code> 头的相关逻辑:</p>
<pre><code>reverse_proxy localhost:8000 {
header_up X-Real-IP {remote_host}
}
</code></pre>
<p>然后修改我们的 Flask 应用的相关代码:(假设我们将对端 IP 存储在变量 <code>remote_ip</code> 中)</p>
<pre><code>remote_ip = request.headers.get(&quot;X-Real-IP&quot;, request.remote_addr);
</code></pre>
<p>这行代码使用了 <code>request.headers.get</code>函数,第一个参数表示我们读取的 HTTP 头是哪个,第二个参数指示默认值(Default Value),也就是假使我们的应用程序没有获取到这个标头,那么仍然有一个默认值,而不是获取到 None 之类完全没有意义的值。</p>
<p>然后就好了?等等,我们外面还套了一层 Cloudflare 呢,我们这样获取到的虽然不是 127.0.0.1 这样的本地 IP,但是获取到的也是 Cloudflare 的 IP,仍然不是客户端的真实 IP。这会有什么问题呢?比如假如我们的应用逻辑中有针对每个 IP 设定的请求次数(QPS)限制之类的东西,我们本来是希望限制该用户(或者攻击者)不能够以超出我们限制的次数发送请求,但是现在这样限制的是 Cloudflare 的 IP 不能够超出我们限制地向我们发送请求,这和我们的本意完全不相符了。正常用户如果运气不好,中间经过的 Cloudflare IP 恰好被限制了,那即使他本人一个请求都没有发送,但就是没办法继续正常使用我们的服务。而攻击者如果想要重复请求我们的服务,根本不需要用换 IP 之类成本高昂的办法,毕竟中间经过的 Cloudflare IP 是什么都不一定呢,每次也可能会变动,相当于可以利用 Cloudflare 庞大的 IP 库绕过我们的 IP 限制。因此,我们必须想办法从 Cloudflare 的请求中获取到客户端原始 IP,然后传递给我们的应用服务。</p>
<p>经验丰富的朋友也可以看出来,在我们上面的网络结构(Waitress &lt;- Caddy (Reverse Proxy) &lt;- Cloudflare &lt;- Client)中,变动代价最小的办法是修改 Caddy 的代码,使它向 Waitress 传递 X-Real-IP 的时候,传递客户端的真实 IP (而不是 Cloudflare 的 IP)。</p>
<p>要做到这点非常简单,根据 <a href="https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ip">https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ip</a> 的文档,Cloudflare 会给每个它发出的请求附带一个 <code>CF-Connecting-IP</code> 的请求头,这里面是客户端的真实 IP。因此我们可以轻易地将上面的 Caddy 代码修改为如下的形式:</p>
<pre><code>reverse_proxy localhost:8000 {
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
}
</code></pre>
<p>这样,代码应该可以在<em>正常</em>环境下正常工作了。不过有什么问题呢?考虑这样的攻击情况:假如攻击者通过某种方式知道或者猜到了我们的源站 IP,然后修改了自己的 <code>hosts</code> 文件,将我们绑定的域名与源站 IP 相关联,这样就可以直接通过源站 IP 来访问我们的服务了,我们的 Caddy 服务器还傻傻地以为它是从 Cloudflare 连入的,试图读取 <code>CF-Connecting-IP</code> 头,不过攻击者此时又不是通过 Cloudflare 连进来的,当然也不能保证 <code>CF-Connecting-IP</code> 头存在,甚至攻击者可以去故意篡改这个头,比如改成 <code>114.51.4.19</code> 这种恶臭 IP 来使我们的数据库变臭(恼),我们必须有办法抵抗这种攻击。</p>
<p>再次感谢 <a href="https://apeiria.net">Misaka13514</a> 和 <a href="https://koishi514.moe/">scientificworld</a> 在相关安全问题上对我的提醒,以及帮助我测试和调试。</p>
<p>要达成这点并不困难。Cloudflare 公布了自己 CDN 的 IP 段:</p>
<ul>
<li>官网介绍页面:<a href="https://www.cloudflare.com/zh-cn/ips/">https://www.cloudflare.com/zh-cn/ips/</a></li>
<li>IPv4:<a href="https://www.cloudflare.com/ips-v4/">https://www.cloudflare.com/ips-v4/</a></li>
<li>IPv6:<a href="https://www.cloudflare.com/ips-v6/">https://www.cloudflare.com/ips-v6/</a></li>
</ul>
<p>我们可以使用上述的列表创建一个白名单,然后修改 Caddyfile,让它验证请求是否来自 Cloudflare,假如不来自,则拒绝请求。示例代码:</p>
<pre><code>example.com {
@cloudflare_ips {
remote_ip 173.245.48.0/20
remote_ip 103.21.244.0/22
...(省略)
}
handle @cloudflare_ips {
reverse_proxy localhost:8000 {
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
}
}
handle {
respond &quot;Only allow connections from cloudflare.&quot; 403
}
}
</code></pre>
<p>这样,在 Cloudflare CDN 关闭或者攻击者知道源站 IP 的情况下,访问我们的服务,只会提示 <code>Only allow connections from cloudflare.</code>,保证了我们的服务只能通过 Cloudflare 访问,也因此一定存在真实正确的 <code>CF-Coneecting-IP</code> 头。</p>
<p>到了这里配置差不多就完成了,不过我们可以再考虑一些特殊情况:Cloudflare Workers 和 WARP。假如攻击者通过 Cloudflare Workers 反代我们的网站,然后在 Worker 代码中尝试篡改 CF-Connecting-IP 头,会发生什么呢?考虑到 Cloudflare Worker 的 IP 段已经被我们刚刚拉入白名单了,攻击者是否会成功呢?</p>
<p>答案是否定的,事实上,Cloudflare 已经考虑过这种特殊情况,防止这种情况的发生。文档地址:<a href="https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ip-in-worker-subrequests">https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ip-in-worker-subrequests</a></p>
<blockquote>
<p>In same-zone Worker subrequests, the value of CF-Connecting-IP reflects the value of x-real-ip (the client’s IP). x-real-ip can be altered by the user in their Worker script.</p>
<p>In cross-zone subrequests from one Cloudflare zone to another Cloudflare zone, the CF-Connecting-IP value will be set to the Worker client IP address &#39;2a06:98c0:3600::103&#39; for security reasons.</p>
<p>For Worker subrequests destined for a non-Cloudflare customer zone, the CF-Connecting-IP and x-real-ip headers will both reflect the client’s IP address, with only the x-real-ip header able to be altered.</p>
<p>When no Worker subrequest is triggered, cf-connecting-ip reflects the client’s IP address and the x-real-ip header is stripped.</p>
</blockquote>
<p>简单来说,攻击者的这种情况属于这里的第二段所描述的情况:攻击者试图从另外一个 Cloudflare Zone 连入。这时为了安全考虑,<code>CF-Connecting-IP</code> 的值会被设定为固定的 <code>2a06:98c0:3600::103</code>。这样的话如果对方使用 Cloudflare Workers 来反代我们的这个服务器,那么我们这边记录的 IP 都是那个固定的 IP,甚至可以用此来针对性屏蔽 Cloudflare Workers;即使不特意屏蔽,因为用 Cloudflare Workers 连进来的 IP 都是完全固定的,所以举个例子,假如我们为每个 IP 设置了请求次数限制(例如:每个 IP 每天最多登录失败 20 次)它仍然是生效的,而且相当于所有试图使用 Cloudflare Workers 的攻击者共享这 20 次限制,事实上并没有降低安全性。而对于 WARP 的情况,目前而言 WARP 的出口 IP 段是 104.28,因此目前还不涉及到这个问题。</p>
<h2>结语</h2>
<p><del>懒得写结尾了,以下总结由 GPT 生成:</del></p>
<p>在这篇文章里,我详细探讨了如何将 Flask 应用部署到生产环境,强调了使用合适的 WSGI 服务器的重要性。由于 Flask 自带的开发服务器并不适合生产使用,是推荐使用 Gunicorn 或 Waitress 作为生产环境的 WSGI 服务器。文章分析了这两者的优缺点,最终选择了 Waitress,并介绍了如何通过反向代理服务器(如 Caddy)来处理 SSL 证书和请求转发。</p>
<p>在与 Cloudflare 配合使用时,想要提醒读者选择适当的 SSL/TLS 模式,特别是避免使用默认的 Flexible 模式,以防止造成无限重定向的问题。此外,文章中还提供了获取客户端真实 IP 的方法,强调了在反向代理配置中确保请求来自 Cloudflare 的重要性,以提高安全性。</p>
<p>最后,文章提到通过白名单 Cloudflare 的 IP 段,能够有效防止直接访问源服务器,确保只有经过 Cloudflare 的请求被允许。这些措施共同提升了应用的安全性和稳定性,使得在生产环境中运行 Flask 应用更加可靠。</p>
]]>
</content>
<summary type="html">当我们完成了一个 Flask 项目,如何将它部署到生产环境中呢?</summary>
<updated>2024-09-01T13:05:51.694Z</updated>
<published>2024-09-01T13:05:51.694Z</published>
</entry><entry>
<title>在 Intel 核显上使用 Stable Diffusion WebUI</title>
<link href="https://nekomoe.xyz/index.html?type=article&filename=stable-diffusion-webui-intel-igpu.md"/>
<id>https://nekomoe.xyz/index.html?type=article&filename=stable-diffusion-webui-intel-igpu.md</id>
<content type="html">
<![CDATA[
<h2>前言:好时代,来临力?</h2>
<p>Stable Diffusion 刚出来的时候我就想玩,不过我笔记本电脑上并没有 Nvidia 显卡,或者更准确的说,没有任何独立显卡,所以只能使用纯 CPU 跑图,生成一张图常常需要好几分钟,非常慢。</p>
<p>忘记去年什么时候就已经听说了 <a href="https://en.wikipedia.org/wiki/OpenVINO">OpenVINO</a> 提供一个支持核显加速的 Stable Diffusion 发行版,但是由于各种原因,一直没能运行成功,这几天终于成功运行起来了。<strong>核显能够跑ai的好时代,终于来临力!</strong></p>
<p>当然,使用 Intel 核显运行 Stable Diffusion WebUI 存在巨大的局限性(见后文),并且即使在核显加速启动的情况下,它的速度也并不十分快(相比纯 CPU 运算快,但不如 NVIDIA 显卡),只是本文希望,对于那些因为各种原因没有 NVIDIA 显卡的环境下(比如我自己),也能体验到 AI 作图的快乐。</p>
<h2>Re: 从零开始的 WebUI 安装过程</h2>
<p>我的环境是 Windows 11 + Intel Iris Xe Graphics。官方同样支持 Linux 和 macOS,同时显卡方面,理论上不太老的 Intel 核显应该都能满足要求。</p>
<h3>环境安装</h3>
<p>首先是安装 Python 3.9 环境,这里我推荐先安装 Anaconda,然后在 Anaconda 中创建 Python 3.9 虚拟环境。本文不是 Python 或 Anaconda 的安装教程,所以这部分暂且略过。以下命令用于使用 conda 创建并激活 Python 3.9 虚拟环境,并假定你已安装 Anaconda.</p>
<p>如果你执行以下命令的时候没有什么效果(虚拟环境没有被激活),那么可能是因为你在使用 PowerShell,需要换成 cmd.exe 再重试执行以下命令。</p>
<pre><code>conda create -n sdwebui_env python=3.9
conda activate sdwebui_env
</code></pre>
<h3>修改 PyPI 镜像源(解决 No matching distribution found for tb-nightly 问题)</h3>
<p>默认源在国内使用会很慢,应该更换一个国内的镜像源。</p>
<p>这里特别提醒:此处<strong>不要</strong>使用清华源。否则后期安装依赖时会遇到 <a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/issues/13363">stable-diffusion-webui Issue#13363</a> 这个问题(ERROR: No matching distribution found for tb-nightly)。</p>
<p>一个可行的解决方案是换用阿里云镜像源,修改命令如下。</p>
<pre><code>pip config set global.index-url https://mirrors.aliyun.com/pypi/simple
</code></pre>
<h3>安装 WebUI 和 OpenVINO Toolkit</h3>
<pre><code>git clone https://github.com/openvinotoolkit/stable-diffusion-webui.git
cd stable-diffusion-webui
pip install --pre openvino
</code></pre>
<ul>
<li><strong>WebUI,启动!</strong></li>
</ul>
<pre><code>.\webui-user.bat
</code></pre>
<h2>开启核显加速</h2>
<p>打开 WebUI,乍一看似乎和正常的 WebUI 没有什么两样?我对普通的 WebUI 没有兴趣!</p>
<p>其实现在核显加速还没有被启用,如果要在生图的时候启用核显加速,还需要在 WebUI 的 Scripts 那里选择 <strong>Accelerate with OpenVINO</strong>,然后在 Select a device 这里选择 GPU,并在这里的 Sampling Method 里重新选一个,因为这里的 Sampling Method 会覆盖你在 Main UI 所选择的采样方法。</p>
<p>然后你可以在 Prompt 中写一些简单的提示词,比如<code>1girl, catgirl, white hair, blue eyes</code>,然后点击生成看看任务管理器中核显是不是在正常工作。注意首次生成的时候会自动重新编译模型+进行预热,而这个重新编译模型和预热的过程中不一定会使用 GPU,并且这整个过程在我的机器上需要大约几分钟时间,但之后每次生成如果没有改变模型本身(具体见下文)的话就不需要再次编译模型了。</p>
<h3>速度</h3>
<p>在我的 Intel Iris Xe Graphic 环境下,以 768x512 的图像生成,DPM++ 2M Karras 作为采样器,Prompt 范围处于 [78, 155],生成模型使用 Anything v3.0 图像模型(文件大小约 4 GB),未添加 LoRA,CFG Scale 为 4,迭代步数设定为 50 的情况下为例:</p>
<ul>
<li>编译模型大约需要 110s 左右(此项仅首次生成需要)</li>
<li>预热大约需要 30s 左右(此项仅首次生成需要)</li>
<li>生成图像大约需要 87s 左右(<code>~1.76s/it</code>)</li>
</ul>
<h3>什么行为会触发模型重新编译?</h3>
<p>目前我简单试了几种情况,会导致模型重新编译的情况包括:</p>
<ul>
<li>修改图片生成尺寸,如:生成尺寸从 512x512 修改到 768x512</li>
<li>更换图像生成模型或添加 LoRA</li>
</ul>
<p>不会导致模型重新编译的情况包括:</p>
<ul>
<li>修改 Prompt</li>
<li>修改 Negative Prompt</li>
<li>修改迭代次数</li>
</ul>
<h2>生成失败了?不关 WebUI 的事哦</h2>
<p>你可能会想之前自己所用的提示词直接复制进去然后尝试生成,然后等着你的可能是两种错误信息:</p>
<ul>
<li>ValueError: prompt_embeds and negative_prompt_embeds must have the same shape when passed directly, but got: prompt_embeds torch.Size([1, 154, 768]) != negative_prompt_embeds torch.Size([1, 77, 768]).</li>
</ul>
<p>这是因为目前 OpenVINO 版的 SD WebUI 要求 <strong>Prompt 的 token 数和 Negative Prompt 的 token 数范围一致</strong>(比如都 ≤77 或都属于 [78, 155])。一些会导致生成失败的情形比如如果 Prompt 有 95 个 token,而 Negative Prompt 没有填写,就会发生此错误。查看 Prompt token 数的方法是在输入框的右上角有一个数字,那就是 token 数。</p>
<p>就我这边的情况来说,会比较容易出现的情况是 Prompt 比 Negative Prompt 的 token 数量更多,从而导致范围不一致。我的解决方法是在原始 Negative Prompt 的后面中不断填入 <code>,bad hands</code> 从而达到使 Negative Prompt token 数量达到和 Prompt token 数范围一致的效果。</p>
<p>然后你可能就会遇到另一个错误了:</p>
<ul>
<li>TypeError: &#39;SymInt&#39; object is not subscriptable</li>
</ul>
<p>这个应该是一个 Bug,怎么解决呢,每次遇到了就直接删除 WebUI 根目录下的 cache 目录即可。</p>
<p>这个 Bug 的触发条件是修改了提示词之后,导致 Prompt token 数范围发生改变。所以修改提示词的时候尽量不要使 Prompt token 数范围发生改变,否则就必须删除 cache 目录让它重新编译模型,耗费很多时间。</p>
<h2>结语</h2>
<p>现在其实有很多方法可以让核显加速 Stable Diffusion 的图像生成,除了本文中提到的来自 OpenVINO 的 Stable Diffusion WebUI 构建之外,这里再推荐一下 <a href="https://mzwing.eu.org/">mzwing</a> 前几天编译的<a href="https://github.com/MZWNET/actions/releases/tag/sd-master-48bcce4">一个支持 CLBLast 的 stablediffusion.cpp 构建</a>,虽然最近比较忙还没有空去尝试这个版本不过应该也可以很好的利用核显的性能,从而带来更快的图像生成。感谢这些开源贡献者,让核显上跑 AI 成为了一种可能。</p>
]]>
</content>
<summary type="html">使用来自 OpenVINO 的 Stable Diffusion WebUI 可以启用核显加速,然而存在一些小问题</summary>
<updated>2024-03-27T00:00:00.000Z</updated>
<published>2024-03-27T00:00:00.000Z</published>
</entry><entry>
<title>我的 2023</title>
<link href="https://nekomoe.xyz/index.html?type=article&filename=my-2023.md"/>
<id>https://nekomoe.xyz/index.html?type=article&filename=my-2023.md</id>
<content type="html">
<![CDATA[
<p><info-hint>本文章中含有大量图片(共约 28.8 MB),请注意流量消耗。</info-hint></p>
<p>坦白地说自大学以来咱就一直处在一个动力不是很足的状态下,不过还是顺利活过了 2023 年,也有很多事情让我开心 或者觉得有意义(</p>
<p>首先是 2023 年咱终于体会到了什么是真正的大学生活,2022 年因为疫情所以几乎什么都干不了,现在看来对我来说影响最大的就是紧闭着的校门导致的不能点外卖或者出去吃,因为学校食堂真的会吃腻(对我来说)而且一些菜确实烧得不好吃。和室友出去吃饭还是很开心的,但是必须承认的一点是即便算是体会到了完全体的大学生活,也与我以前的想象差距不小。比如,大学虽然有很多自由时间和精力,但是不意味着自己就会进入自主学习或者更上进的状态,也就是说拖延症这种问题并不会因为上了大学自行解决()自控力还在其次,性格或者成长上的进步也需要自己去努力,而不是光是靠更轻松的生活,就觉得这些问题都直接都被解决了。</p>
<p>还有是今年参加了一些 CTF 比赛,作为一个自认为高中生活本身比较无趣(高中没有计算机社团,也没有参加过什么相关比赛)的人来说可以说是第一次的初体验了,不但拿了奖还获得了一笔对我来说数目不算小的奖金,开心()这里也十分感谢几个队友在比赛过程中对我的帮助(我比较容易紧张)以及在分奖金时候对我的照顾;2023 年一共参加过两次 CTF 和一次 AWD 形式的比赛,还因为这几次比赛的缘故有幸得到了学校的注意,并体验了学校的护网工作。帮大忙了.webp(指参加比赛给我的前途帮大忙了,不要理解成给学校的护网工作帮大忙了,其实摸了很多鱼(</p>
<p>要说参加 CTF 给我成长的最大帮助应该就是意识到了很多事情没有看上去那么难,只要有尝试的勇气和信心。也许就像《星灵感应》里面的火箭那样,开始尝试做了之后才会发现没有自己想象的遥不可及。以前一直以为搞 OI 的大佬或者玩 CTF 的这种人都是大佬中的大佬,现在自己也参加过比赛了也能更客观的看待这些比赛了()除了 CTF 比赛之外没有参加其他类型的比赛,但是已经报了下一年的 Python 组蓝桥杯,不知道到时候能拿个什么水平的奖。犹豫过要不要报 C/C++ 组但是我比较摸所以不一定能在比赛之前把语言基础学到足够的水平(</p>
<p>年初的时候学了 C#,不过客观来说自己还是用 JavaScript(TypeScript)和 Python 比较多。2023 年写过一个<a href="https://nekomoe.xyz/index.html?type=article&filename=a-failure-in-web-ocr.md">比较失败的 Web 端 OCR 程序</a> 以及一个技术力比较低的新的<a href="https://i.nekomoe.xyz">个人主页</a>(用来替代我原先使用的 <a href="https://github.com/amphineko/atomicneko">amphineko/atomicneko</a>),别的好像也没什么特别的,翻了一下才知道自己还往一些开源仓库水了一些(大概是可有可无的)贡献,我记性也不好可能写过别的也忘了()这方面做的事情确实没有 2022 年多,主要是缺乏动力以及客观上的技术力下降(?)(</p>
<p>然后是其他的折腾方面,首先是电脑上,目前正在使用的是 Ubuntu 23.10 + GNOME 45.1。在最近我开始有意识地更多地使用开源软件,特别是 LibreOffice。我之前用的比较多的其实是 WPS Office(因为和 MS Office 的兼容性好并且支持 Linux),但是今年开始更多地使用 LibreOffice 了。促使我使用 LibreOffice 的最大原因是 WPS 对 Linux 的支持问题:WPS 不支持 Wayland,所以它的所有文本和按钮在我电脑上看起来都是糊的。另外,FreeType 最近的一次更新导致 WPS 的所有粗体文本都会显示成一大坨黑色的不规则块。总之试着换了一下 LibreOffice,这里我主要担心的是格式兼容问题。不过在我的使用场景(交作业)上,对于 Word 来说,格式与 MS Office 有些许微小的差异其实并不是什么大问题。主要的问题集中在 Excel 上。因为这学期我学了一门有关会计的课,才发现有些 Excel 函数在 LibreOffice 上的行为其实是不同的,所以对于 Excel 来说还是要用 WPS 或者一些其他解决方案。</p>
<p>以及这里想要特别地提一下 Wine 和 Proton。现在的 Wine(Proton) 已经非常强大了,我的 Galgame 几乎都是通过 Wine 运行的,目前很少遇到运行不起来的情况。除此之外我需要用的许多游戏和程序也能通过 Wine 顺利运行,比如说 osu!、网易云音乐客户端等等。</p>
<p>然后是手机上,八月份的时候我人生以来第一次尝试给安卓手机刷机以及 Root。我用的是三星的 Galaxy M30s,这部手机比软好的是解锁 BL 比较方便(三星手机特有的容易解锁,你是一个一个一个,三星手机啊啊啊啊啊啊);随后尝试用三星的 ROM 刷写工具 OdinFlashTool 将 TWRP 刷写入手机中。(这里插一个小插曲:因为我当时用的是 Linux,不能运行 OdinFlashTool,所以很自然地想到了开源解决方案(有两个,JOdin 和 Heimdall),然后很轻松地......把手机刷砖了,这里还是建议用 Windows 操作,或者至少在 Windows 虚拟机中使用 USB 直通进行操作,我也不知道为什么 JOdin / Heimdall 会有这种问题 )。另外,大概是因为用的是国行版本的缘故(因为 PixelExperience 似乎并没有特别为国行版本适配),刷 PixelExperience 的尝试并没有成功,但是最终还是找到了一个比较轻量的 ROM 包刷入了。尝试进行了 Root 并安装了 Magisk,但是因为银行 APP 等不支持 Root,而一些规避方案 (包括使用旧版 Magisk 的 MagiskHide 功能)都会导致系统没法启动,最终作罢。估计是我选择的 ROM 的问题。这里也很感谢 <a href="https://blog.apeiria.net/">@Misaka13514</a> 在我折腾 Android 的时候不厌其烦地不断回答我一些现在看来很蠢的问题(</p>
<p>还有一个想提的就是开始使用 YubiKey 了。YubiKey 是 <a href="https://blog.apeiria.net/">@Misaka13514</a> 三月份的时候送给我的,我目前使用 YubiKey 的用途有两个,一个是将其作为网站的两步验证因素之一,另一个是用它来存储自己的 OpenPGP 私钥。具体的我在<a href="https://nekomoe.xyz/index.html?type=article&filename=2023-5.md">之前的文章</a>中有写过;以及学习了 DN42 并熟悉了 WireGuard、BIRD2 等软件的使用,还获得了一个不错的 DN42 域名 <code>neko.dn42</code>,也是 <a href="https://blog.apeiria.net/">@Misaka13514</a> 解答了许多我的问题。感谢 <a href="https://blog.apeiria.net/">@Misaka13514</a> 在各个方面给我的帮助!</p>
<p>AI 方面的话,自己尝试部署了 Stable Diffusion 并学习了基本使用,<del>虽然我用的笔记本是核显本只能用 CPU 演算不过不是不能用</del>。用 Stable Diffusion 给我做过的两个开源项目(<a href="https://github.com/bbg-contributors/bbg">这个</a>和<a href="https://github.com/baiyuanneko/bocchi">这个</a>生成了图标)。以及因为我平时看的一部分动画是生肉,或者视频语言是日语而没有字幕,想到了使用 <a href="https://github.com/openai/whisper">Whisper</a> 去生成字幕并丢给 ChatGPT 翻译,效果还不错。</p>
<p>以及,因为 B 站上有很多 AI 翻唱的,于是自己尝试训练了一个小之星海果的 so-vits-svc 模型(仅供我自己个人研究学习使用,不会公开),效果只能说差强人意不过考虑到我的时间和精力有限,自己剪出来的训练数据质量也不是很高所以也不是特别失望。</p>
<p><img src="./2023Summary_SoVitsSvc_01.png" alt="image"></p>
<p>2023 年我开始接触更多游戏<del>以及开始给Steam爆金币了</del>。2023 年我开始玩的几个在线游戏包括Blue Archive(国服)、赛马娘台服、原神、雀魂麻将等等。赛马娘是因为有点占空间(占了我 10G 甚至 9G)于是没有坚持玩下去。原神我个人觉得还是挺好玩的,原因的话大概是制作精美的二次元风格自机角色 + 清新的渲染风格 + 开放世界 + RPG + 整洁不杂乱的界面 + 我觉得还不错的剧情 这些因素共同叠加产生的奇妙化学反应。不过因为 Linux 上的原神国服使用 Wine 运行经常是处于一个时好时不好的状态所以还是经常是需要使用云原神的。玩 Blue Archive 的主要原因之一是因为之前看到过才羽绿和才羽桃的插画,感觉真的<strong>好可爱好可爱</strong>耶,瞬间被治愈了。虽然也没有抽到全部我想抽的角色,不过剧情比我想象的要更好,比如主线第二章的游戏开发部部分剧情,还是有一种久违的感动感的(至少对我来说)。感谢我的一个同学带我玩 Blue Archive,没人教的话我甚至都不会清体力()最后就是雀魂麻将了。打了一年麻将,也不能说技术提高了很多,还是很菜,不过抽到了一个有可爱语音的角色(当然别的地方也挺可爱的),好耶。</p>
<p><img src="./2023Summary_GenshinImpact_01.png" alt="image"></p>
<p><img src="./2023Summary_MahjongSoul_01.png" alt="image"></p>
<p><img src="./2023Summary_MahjongSoul_02.png" alt="image"></p>
<p><img src="./2023Summary_MahjongSoul_03.png" alt="image"></p>
<p><img src="./2023Summary_MahjongSoul_04.png" alt="image"></p>
<p><img src="./2023Summary_BlueArchive_01.jpg" alt="image"></p>
<p><img src="./2023Summary_BlueArchive_02.jpg" alt="image"></p>
<p><img src="./2023Summary_BlueArchive_03.jpg" alt="image"></p>
<p><img src="./2023Summary_BlueArchive_04.jpg" alt="image"></p>
<p>然后是 Steam 上,终于开始买一些游戏了,首先是补票了 NEKOPARA 的全系列,然后买了一直想玩的《主播女孩重度依赖》(不过目前只通关了14/24个结局),然后是买了一些 Galgame (《恋爱绮谭 不存在的夏天》、《恋爱绮谭 不存在的真相》以及对我来说有点贵的《常轨脱离 Creative》),以及《城市:天际线》(但是还没开始玩)这里面对我来说觉得最值回票价的反而是最贵的《常轨脱离 Creative》,几位女主都很可爱,<code>最享受的一集!.webp</code> 当然我这个 Steam 库存量也算少的,也不是说要攀比或者什么,只是随便分享一下((</p>
<p><img src="./2023Summary_SteamLibrary.png" alt="image"></p>
<p>其它游戏方面,玩了《Undertale》、《Getting Over It》之类的一些独立游戏。以及在时断时续地玩着《osu!》(以及《osu!lazer》),我打这个水平不是很高但是消磨时间还是一个不错的选择(以及,很荣幸能把 <a href="https://blog.chihuo2104.dev/">@chihuo2104</a> 拉入 osu! 的坑(x</p>
<p><info-hint>下面两张图中上面一张是 osu!stable 的个人资料,下面一张是 osu!lazer 的个人资料。</info-hint></p>
<p><img src="./2023Summary_osu_01.png" alt="image"></p>
<p><img src="./2023Summary_osuLazer_01.png" alt="image"></p>
<p>《Minecraft》方面,寒暑假的时候玩了群友开的服务器(</p>
<p>第一次尝试了在 Minecraft 中搭建手办(参考了 <a href="https://www.bilibili.com/video/BV1yW411g7ji/">这个视频</a>),同时也第一次尝试用 <a href="https://www.scriptolab.com/minecraft-pixel-art-en.php">Minecraft Pixel Art Generator</a> 在 Minecraft 中绘了图,也感谢 <a href="https://blog.apeiria.net/">@Misaka13514</a> 在绘图过程中的大量帮助。</p>
<p>感谢 <a href="https://blog.apeiria.net/">@Misaka13514</a>、<a href="https://koishi514.moe/">@scientificworld</a>、<a href="https://blog.chihuo2104.dev/">@chihuo2104</a>、<a href="https://littlesunnybear.com/">@littlebear</a>、<a href="https://mzwing.eu.org/">@mzwing</a>、<a href="https://github.com/zzjzxq33/">@zzjzxq33</a>、<a href="https://kolozea.top">@Kolozea</a>、<a href="https://space.bilibili.com/516252009/">@666999HC</a> 等群友(可能有漏的?)陪我玩 Minecraft!</p>
<p>年底的时候通过 Microsoft Store 入了正。</p>
<p><img src="./2023Summary_Minecraft_01.png" alt="image"></p>
<p>在这一年里,还和 <a href="https://blog.apeiria.net/">@Misaka13514</a> 和 <a href="https://koishi514.moe/">@scientificworld</a> 使用 Syncplay 同步看了 MyGo!!!!! 以及其他许多番剧,我今年大约三分之二的番剧都是通过 Syncplay 和群友 <a href="https://blog.apeiria.net/">@Misaka13514</a> 和 <a href="https://koishi514.moe/">@scientificworld</a> 一起看的,感谢你们的陪伴!(</p>
<p><img src="./2023Summary_Syncplay_01.png" alt="image"></p>
<h3>音乐相关</h3>
<p>感谢 <a href="https://blog.chihuo2104.dev/">@chihuo2104</a> 推荐给我的竹月雨 II 耳机,说是 <a href="https://mujitogawa.github.io/">@MujiTogawa</a> 推荐给他的,我用着感觉也很好用。</p>
<p><img src="./2023Summary_Music.jpg" alt="image"></p>
<h3>小彩蛋 1</h3>
<p>Hackergame 2023 临近结束的时候被 <a href="https://tqlwsl.moe/">@wlt233</a> 超力(悲)(雾</p>
<p><img src="./2023Summary_Hackergame.png" alt="image"></p>
<h3>小彩蛋 2</h3>
<p>只玩了大概一个多小时的 Phigros()好难(</p>
<p><img src="./2023Summary_Phigros.jpg" alt="image"></p>
<h2>See also</h2>
<p>也可以看看其它人的年终总结:</p>
<ul>
<li><a href="https://blog.apeiria.net/2023/my-2023/">我的 2023 - 欠陥電気の摸鱼小池</a></li>
<li><a href="http://koishi514.moe/blog/bbg/index.html?type=article&filename=s5e6TSCZKQXW.md">我的 2023 年终总结 - 无意识の桜</a></li>
<li><a href="https://mzwing.eu.org/index.html?type=article&filename=small-article-20231209.md">小碎记20231209 - 洛仙璃の幻梦</a></li>
<li><a href="https://mjt.asia/posts/ed8b190a/">Byebye 2023(或 17岁再见) - 木屐落在水洼了</a></li>
<li><a href="https://blog.chihuo2104.dev/posts/goodbye-2023">2023年度总结 - chihuo2104の部落格</a></li>
<li><a href="https://tqlwsl.moe/index.php/archives/2778/">【碎碎念】某魏的 2023 年度总结</a></li>
<li><a href="https://sekaimoe.dpkg123.site/end-of-2023/">SekaiMoe 的 2023 - SekaiMoe&#39;s World</a></li>
<li><a href="https://www.ccrice.com/neworld/761/2023%e7%9a%84%e6%80%bb%e7%bb%93/">2023的总结 - CC米饭</a></li>
<li><a href="https://blog.mashiro.pro/1662.html">给2023画上一个句号 - 白のblog</a></li>
<li><a href="https://blog.mk1.io/posts/bye-2023">迟来了一个月的 2023 年度总结 + 2024 新年快乐! - Ray&#39;s Blog</a></li>
</ul>
<h2>结语</h2>
<p>希望自己能在新的一年里技术力越来越高,也祝正在看这篇文章的读者新年快乐!</p>
]]>
</content>
<summary type="html">我的 2023</summary>
<updated>2024-01-25T00:00:00.000Z</updated>
<published>2024-01-25T00:00:00.000Z</published>
</entry><entry>
<title>记录一次失败的 Web 端 OCR 尝试</title>
<link href="https://nekomoe.xyz/index.html?type=article&filename=a-failure-in-web-ocr.md"/>
<id>https://nekomoe.xyz/index.html?type=article&filename=a-failure-in-web-ocr.md</id>
<content type="html">
<![CDATA[
<p>想特别地缅怀(雾)一下我之前进行的比较失败的 Web 端离线 OCR 尝试。</p>
<p>想做这个的起因是因为虽然网页端有很多在线 OCR 的,但是它们大多数都是上传到服务器端进行的。即使它写了服务器端不会保存你的图像数据,不过这种事谁说得准呢()于是就开始了我的折腾。首先这种场景大概率是要用到 WebAssembly 的,于是直接在 Google 上搜索 <code>ocr wasm</code>。第一个结果就是 tesseract-wasm,看起来非常不错,而且还提供了使用的 Example,只是它的示例上不支持中文。但是示例不支持并不代表技术上不支持,反正本质都是 Tesseract,直接把英语模型换成中文模型其实就能正常工作了。</p>
<p>最核心的 OCR 部分既然是可以实现的,那么剩下的就是基于这个方向上继续做了。</p>
<h2>获取剪贴板中的图像</h2>
<p>对于 OCR 软件来说,能让用户直接对剪贴板中的图像进行 OCR 是一个极为常见的需求。但是这里有一个小问题,就是获取剪贴板中的图像的实现方式,如果直接 Google,你大概会注意到的第一个结果是 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard">Clipboard API</a>,但是如果你真的去试了一下的话就会发现,Firefox 几乎不支持它(只支持 writeText 方法,其余的包括 write、readText、read 等都不支持)。</p>
<p>所以还是需要用比较传统的方式,监听粘贴事件然后从事件中读出剪贴板数据。但是这样的话就必须要用户按下粘贴快捷键,而不是我可以实现一个网页上的按钮,点击之后浏览器向用户征求权限,然后得到权限之后直接读取剪贴板,感觉并不是很优雅。</p>
<pre><code>document.addEventListener(&quot;paste&quot;, async function (event) {
const items = (event.clipboardData)?.items;
if (items !== undefined) {
for (let item of items) {
if (item.kind === &quot;file&quot;) {
const blob = await new Response(item.getAsFile()?.stream()).blob();
if (blob !== undefined) {
const bitmap = await createImageBitmap(blob);
tryIdentifyImageBitmap(bitmap);
break;
}
}
}
}
}
</code></pre>
<p>题外话,Firefox 其实不支持一些 Web API,例如 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard">Clipboard API</a>、<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/File_System_API">File System API</a> 等等,但它们其实还是挺有用的。根据<a href="https://github.com/mozilla/standards-positions/issues/154">这篇 GitHub 讨论</a>,Firefox 不支持 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/File_System_API">File System API</a> 的原因主要是因为安全方面的考虑,因为让网站能访问用户的本地磁盘确实存在危险性(相比之下,如果是网站不能直接访问,需要通过本地程序来进行的话,必然需要下载东西,而下载文件就可以受到 SmartScreen 等安全机制的审查,风险会小很多),不过不支持 Clipboard API 我没找到相关讨论,大概也是安全方面的原因吧。Firefox 所不支持的这些 API 对我个人来讲其实无所谓,因为我用不到需要这些权限的网站(比如 <a href="https://vscode.dev">https://vscode.dev</a> 我并没有使用它的需求),不过还是希望 Firefox 能支持这些 API。而且比如说 Google 翻译的翻译图像功能就我写文章的时候也是采用的 Clipboard API 来读取剪贴板中的图像的,所以这个功能在 Firefox 上也是用不了的,我用这个功能的时候还要切换到 Google Chrome 上去。最近有个关于 Firefox 的新闻说假如 <a href="https://analytics.usa.gov/data/live/browsers.json">https://analytics.usa.gov/data/live/browsers.json</a> 这里的占有率下降到 2% 以下的话 <a href="https://usa.gov/">https://usa.gov/</a> 就不会在支持 Firefox 了,这可能引发破窗效应导致许多网站放弃支持 Firefox。不过我觉得如果这个情况真的发生了,Firefox 总会做出改变,所以我个人倒没有特别担心。对于我来说更重要的是 Google Chrome 现在倒是在启用 Wayland 模式的时候中文输入法又坏了,相比之下 Firefox 对 Linux 支持一直都很好。</p>
<h2>拖拽区域</h2>
<p>另一个常见的需求是支持拖拽图像到程序中。同样是随便抄了一段代码,效果也还可以。</p>
<pre><code>
if (uiMainArea !== null) {
uiMainArea.addEventListener(&quot;drop&quot;, async function (event) {
event.preventDefault();
event.stopPropagation();
const files = event.dataTransfer?.files;
if (files !== undefined) {
for(let file of files){
if(file.type.indexOf(&quot;image&quot;) &gt; -1){
const bitmap = await createImageBitmap(await new Response(file.stream()).blob());
tryIdentifyImageBitmap(bitmap);
break;
}
}
}
})
uiMainArea.addEventListener(&quot;dragover&quot;, function (event) {
event.preventDefault();
event.stopPropagation();
});
}
</code></pre>
<h2>从计算机上选择(上传)图像</h2>
<p>这个是比较常见的需求,实现的方法也很多,这里也是随便抄了一段。</p>
<pre><code>if (selectImageBtn !== null){
selectImageBtn.onclick = function(){
const input = document.createElement(&quot;input&quot;);
input.type = &quot;file&quot;;
input.accept = &quot;image/*&quot;;
input.onchange = async function(event){
const files = input.files;
if(files !== null){
for(let file of files){
if(file.type.indexOf(&quot;image&quot;) &gt; -1){
const bitmap = await createImageBitmap(await new Response(file.stream()).blob());
tryIdentifyImageBitmap(bitmap);
break;
}
}
}
}
input.click();
}
}
</code></pre>
<h2>模型的储存</h2>
<p>Tesseract 的中文模型大小大约 50 M,要说到存储,我首先想到的是包含模型的 ArrayBuffer 转成字符串然后存储在 LocalStorage 中(因为 LocalStorage 的值是要求以字符串的形式存储的),然后需要用的时候再读出来。</p>
<blockquote>
<p>另外,localStorage 中的键值对总是以字符串的形式存储。 (需要注意,和 js 对象相比,键值对总是以字符串的形式存储意味着数值类型会自动转化为字符串类型).
—— MDN 文档</p>
</blockquote>
<p>不过 LocalStorage 并不适合存储这么多数据,而且 ArrayBuffer 和字符串如何进行转换呢?</p>
<p>很自然地,可以找到 <a href="https://www.npmjs.com/package/arraybuffer-to-string">arraybuffer-to-string</a> 这个 NPM 包。不过这个用起来在我的环境下会报错,怀疑是因为内容量太大导致转换有问题或者是导致资源不足没办法进行转换。而且明明 ArrayBuffer 是一个很方便的东西,却被搞成了这个样子(指被转换成了字符串),真是痛心疾首,愧对浏览器,愧对内存(bushi)</p>
<p>其实这种场景是应该使用 IndexedDB 的,包括 MDN 在 Service Worker 的使用指南中也提到了这一点。这个东西其实上手有些难度(至少对我来说),不过已经有一些封装好的库了(像是 LocalForage)所以也不会特别困难。</p>
<pre><code>
const getModelBuffer = async function (): Promise&lt;ArrayBuffer&gt; {
return (await localforage.getItem(&quot;models&quot;) as object)[&quot;chi_sim&quot;];
}
const isModelDataInCache = async function (): Promise&lt;boolean&gt; {
if (await localforage.getItem(&quot;models&quot;) !== null &amp;&amp; (await localforage.getItem(&quot;models&quot;) as object)[&quot;chi_sim&quot;] !== null) {
return true;
} else {
return false;
}
}
const startModelDownload = function (bitmapToIdentifyThen: ImageBitmap): void {
downloadingModelModal.show();
const xhr = new XMLHttpRequest();
xhr.responseType = &quot;arraybuffer&quot;;
xhr.open(&#39;GET&#39;, &quot;./models/chi_sim.traineddata&quot;, true);
xhr.addEventListener(&#39;progress&#39;, (event) =&gt; {
if (event.lengthComputable) {
const percentage = (event.loaded / event.total) * 100;
updateProgressOfModelDownloading(percentage);
}
});
xhr.addEventListener(&#39;load&#39;, async () =&gt; {
await localforage.setItem(&quot;models&quot;, { &quot;chi_sim&quot;: await xhr.response });
downloadingModelModal.hide();
startIdentifyImageBitmap(bitmapToIdentifyThen);
});
xhr.addEventListener(&#39;error&#39;, () =&gt; {
console.error(&#39;Error during download&#39;);
});
xhr.send();
}
</code></pre>
<h2>PWA 支持</h2>
<p>按照 MDN 文档的示例写一个 Manifest 和 Service Worker 即可。不过 MDN 的示例是缓存优先的策略,那么网络优先的策略呢?一番搜索后可以找到 <a href="https://developer.chrome.com/docs/workbox/caching-strategies-overview/#the-cache-interface-versus-the-http-cache">https://developer.chrome.com/docs/workbox/caching-strategies-overview/#the-cache-interface-versus-the-http-cache</a> 这个教程,直接抄上来就完了。</p>
<p><code>manifest.json</code>:</p>
<pre><code>{
&quot;name&quot;: &quot;bocchi&quot;,
&quot;start_url&quot;: &quot;.&quot;,
&quot;display&quot;: &quot;standalone&quot;,
&quot;theme_color&quot;: &quot;#cc3366&quot;,
&quot;description&quot;: &quot;一个现代的 OCR 工具。&quot;,
&quot;icons&quot;: [
{
&quot;src&quot;: &quot;icon.png&quot;,
&quot;type&quot;: &quot;image/png&quot;,
&quot;sizes&quot;: &quot;512x512&quot;
}
]
}
</code></pre>
<p><code>sw.js</code>:</p>
<pre><code>const cacheName = &#39;cache_v1&#39;;
self.addEventListener(&#39;install&#39;, (event) =&gt; {
event.waitUntil(
caches.open(cacheName).then((cache) =&gt; {
return cache.addAll([
&#39;/bootstrap.min.css&#39;,
&#39;/{{BUNDLED_JS_FILENAME}}&#39;,
&#39;/icon.png&#39;,
&#39;/manifest.json&#39;,
&#39;/tesseract-worker.js&#39;,
&#39;/tesseract-core.wasm&#39;,
&#39;/tesseract-core-fallback.wasm&#39;
]);
})
);
});
self.addEventListener(&#39;fetch&#39;, (event) =&gt; {
if (event.request.mode === &#39;navigate&#39;) {
event.respondWith(
caches.open(cacheName).then((cache) =&gt; {
return fetch(event.request).then((fetchedResponse) =&gt; {
cache.put(event.request, fetchedResponse.clone());
return fetchedResponse;
}).catch(() =&gt; {
return cache.match(event.request);
});
})
);
} else {
event.respondWith(
caches.match(event.request).then((cachedResponse) =&gt; {
return cachedResponse || fetch(event.request);
})
);
}
});
</code></pre>
<p>以及别忘了 register <code>sw.js</code>:</p>
<pre><code>const registerServiceWorker = async () =&gt; {
if (&quot;serviceWorker&quot; in navigator) {
try {
const registration = await navigator.serviceWorker.register(&quot;/sw.js&quot;, {
scope: &quot;/&quot;,
});
if (registration.installing) {
console.log(&quot;正在安装 Service worker&quot;);
} else if (registration.waiting) {
console.log(&quot;已安装 Service worker installed&quot;);
} else if (registration.active) {
console.log(&quot;激活 Service worker&quot;);
}
} catch (error) {
console.error(`注册失败:${error}`);
}
}
};
registerServiceWorker();
</code></pre>
<h2>移除识别结果中中文之间的空格</h2>
<p>还有一个问题,就是在识别中文的时候识别结果始终会在任意两个汉字之间有一个没有意义的空格,英文则可以正常地加入空格。这显然完全不符合中文阅读习惯和要求。因此,我们需要去除中文字之间多余的空格,但保留英文之间正确的空格。</p>
<p>问了一下 ChatGPT,给出了这样的答案:</p>
<pre><code>const removeSpacesAroundChinese = (text: string) =&gt; {
const result = text.replace(/(\S)\s+(\S)/g, function (match, p1, p2) {
// 只有当空格的左边和右边都是中文字符时才替换为空字符串
if (/[\u4e00-\u9fa5]/.test(p1) &amp;&amp; /[\u4e00-\u9fa5]/.test(p2)) {
return p1 + p2;
} else {
return match;
}
});
return result;
};
</code></pre>
<p>使用<code>/[\u4e00-\u9fa5]/</code>这个正则表达式来识别中文字符还是挺神奇的,学到了。但是这段代码测试下来有一个问题,就是每次只能去除一部分空格,不过我懒得细究原因了,既然每次至少能去除一部分空格,那么我们循环进行这个过程直到所有空格都被去除即可。于是可以编写如下的函数,循环调用<code>removeSpacesAroundChinese</code>函数直到前后两次调用的结果相等:</p>
<pre><code>const removeSpacesAroundChineseRecursively = (text: string) =&gt; {
let first_text = text;
let second_text = removeSpacesAroundChinese(first_text);
while (first_text !== second_text) {
first_text = second_text;
second_text = removeSpacesAroundChinese(first_text);
}
return second_text;
}
</code></pre>
<h2>使用效果</h2>
<p><img src="./images/webocr.png" alt="image"></p>
<p><img src="./images/webocr-result.png" alt="image"></p>
<h2>问题</h2>
<p>使用之后不难发现一个问题:识别率比较低下。这个其实是 Tesseract 本身识别率比较低的原因。Tesseract 本身发布是一个比较早的框架,虽然也在不断进步,比如在 Tesseract 4 中使用了 LSTM 神经网络技术,但是相比现在更流行的 PaddleOCR 可能存在一定的落后吧。(我对 OCR 技术了解不多,这方面不是很懂)</p>
<p>另外 Tesseract 本身设计是为了识别非常干净整洁的图像的,也要求使用者在使用之前先对图像进行某种程度上的处理。比如根据我查阅到的资料来看,需要进行去噪,二值化、旋转至 90 度等操作。并且在必要的时候应该先识别出带有文字的行,然后把每个行截取出来处理好后使用 Tesseract 的单行模式依次进行识别然后再合并,而不是直接进行识别。而这些操作都是我所没有进行的。由于精力原因也没有进行(考虑到,网络上的教程大多数是基于 Python 给出的示例,在浏览器端进行等效操作需要查阅另外的资料,虽然最后也能实现)。</p>
<p>之后我也尝试了使用 PaddleOCR 的 <code>paddle.js</code>。不过它给出的示例在我的 Firefox 上并无法正常运作。</p>
<pre><code>Error: Requested texture size [10240x16] greater than WebGL maximum on this browser / GPU [8192x8192].
</code></pre>
<p>另外,使用 Chrome 尝试后对于英文部分的识别效果有一些问题(与 Tesseract 相反,PaddleOCR 的问题是不加空格),可能和浏览器端运行的模型大小比较小有关。</p>
<h2>其它的一些杂七杂八</h2>
<p>在这个东西做好之后,我其实在考虑托管到哪里。因为模型文件比较大(50M),这已经超出了 Cloudflare Pages 的单文件大小限制。虽然可以分拆模型但是感觉有点像 Abuse,就不这样操作了。<a href="https://github.com/Misaka13514">Misaka13514</a> 倒是提醒了我 GitHub Pages 的大小限制更宽松一些,但是一个是,GitHub Pages 的速度太慢了,再一个,我讨厌让网站构建完成的文件(<code>dists/</code>)出现在 Git 仓库的任何位置(即使它不位于 <code>main</code>/<code>master</code> branch 我也不喜欢,不过我研究了半天也没搞明白怎么做)。最后还是放到我自己的服务器上了。第一次尝试 Caddy,感觉比 nginx 方便很多,尤其是 SSL 自己就配置好了,我一点脑子也不用动。现在想来其实还可以设置成(让用户)从 tessdata 的 GitHub Releases 那里获取模型文件,这样我就完全不必托管 50M 的模型文件了,不过我懒的修改代码了,再加上这样的话估计国内用户下载一天也不一定能把模型下载完。</p>
<p>如果有谁还想试用一下这个有点失败的尝试的话,可以前往 <a href="https://ocr.nekomoe.xyz/">https://ocr.nekomoe.xyz/</a>。网页图标是我使用 Stable Diffusion 配合一个下载的 LoRA 制作的。最终效果至少我自己还是很满意的。这里放上我所使用的 Prompt 和相关参数:</p>
<pre><code>Prompt: (pixelart:1.2), bright colours, 1girl, gotoh hitori, pink hair, long hair, hair between eyes, blue eyes, cube hair ornaments, pink clothes, close-up, sunlight, snowy, falling snow
Negative prompt: hands
Steps: 24
Sampler: DPM++ 2S a
CFG scale: 5
</code></pre>
]]>
</content>
<summary type="html">想做这个的起因是因为虽然网页端有很多在线 OCR 的,但是它们大多数都是上传到服务器端进行的。</summary>
<updated>2024-01-07T00:00:00.000Z</updated>
<published>2024-01-07T00:00:00.000Z</published>
</entry><entry>
<title>最近的一些事</title>
<link href="https://nekomoe.xyz/index.html?type=article&filename=2023-5.md"/>
<id>https://nekomoe.xyz/index.html?type=article&filename=2023-5.md</id>
<content type="html">
<![CDATA[
<p>距离我上次写博客已经隔了将近五个月的时间了,我这段时间不更新的原因其实是 <del>单纯就是懒得写(误)</del> 一方面我这几个月确实没特别钻研过什么复杂的东西,水平也不到位,虽然可以「为了让自己的博客看上去充实而作为一个自己也刚入门某个东西的人还要去写一篇类似XX入门指南的东西」,但是我自己是不想这么做,而写写自己的生活或者评析一下看过的动画/玩过的Galgame这种文章我写起来也没有之前写这类文章的那种感觉了。我试着推理一下可能的原因,其实现在这个博客刚刚建立是在2021年的8月份,从这个时间一直到去年的7月份就是我上高三的时间,那段时间里在各种大考小考中写了无数篇议论文,虽然挺累的,但是这种频繁的训练大概也激发了我写点什么东西的冲动,而到了现在我就变懒了,乐。</p>
<p>下面我就随便说说这几个月里我觉得值得说一说的事情。</p>
<h2>开始使用 YubiKey</h2>
<p>三月份的时候 <a href="https://i.apeiria.net/">Misaka13514</a> 送了我一个 YubiKey 5C NFC,从此就开始了我使用 YubiKey 的经历。那么什么是 YubiKey 呢?请看下面的介绍。</p>
<blockquote>
<p>YubiKey是由Yubico生产的身份认证设备,支持一次性密码(OTP)、公钥加密和身份认证,以及由FIDO联盟(FIDO U2F)开发的通用第二因素(U2F)协议。它让用户可以透过提交一次性密码或是使用设备产生的公开/私密金钥来安全地登录自己的帐户。针对不支持一次性密码的网站,YubiKey也可以存储静态密码。Facebook使用YubiKey作为员工凭证;Google同时为雇员和用户提供支持。还有一些密码管理器也支持YubiKey。
--- 维基百科<ref url="https://zh.wikipedia.org/wiki/YubiKey">YubiKey - 维基百科</ref></p>
</blockquote>
<p>我入门 YubiKey 的时候除了官方的教程,主要参考的文章是这一篇:</p>
<ul>
<li><a href="https://blog.gimo.me/posts/getting-started-with-yubikey/">开始使用 YubiKey | Yuanji&#39;s Blog</a></li>
</ul>
<p>我目前使用 YubiKey 的用途有两个,一个是将其作为网站的两步验证因素之一,另一个是用它来存储自己的 OpenPGP 私钥。对于两步验证和 OpenPGP 这两个概念不在这里过多介绍,简单来说前者指的是在登录网站账户的时候除了账户密码还同时要求另一个方式来验证,后者比较复杂,如果你完全不知道的话可以看<a href="https://zh.wikipedia.org/wiki/PGP">这个来自维基百科的链接</a>做进一步了解。</p>
<p><info-hint>虽然不用 YubiKey 这样的硬件安全密钥也可以设置两步验证或者比较安全的存储 OpenPGP 私钥,但是实际上在做这些事的时候 YubiKey 比传统方式安全得多。因为不管是 TOTP 还是 FIDO U2F 还是使用 GPG 加密/解密/签名文件,传统方式都必须要将 TOTP 私钥 / OpenPGP 私钥 / 其它各种类型的私钥存储在设备本地,如果相关应用没有妥善地存储私钥(比如不加密/加密方式极其简单/加密所用的密钥泄露)就有可能被恶意软件窃取。而对于 YubiKey 来说私钥一旦被导入 YubiKey 中就无法再次读出,确保了私钥不会泄露。<ref url="https://developers.yubico.com/U2F/Protocol_details/Key_generation.html">Key generation</ref><ref url="https://support.yubico.com/hc/en-us/articles/360013790259-Using-Your-YubiKey-with-OpenPGP">Using Your YubiKey with OpenPGP - Yubico</ref><ref url="https://developers.yubico.com/OATH/">OATH</ref></info-hint></p>
<p>具体到我的使用体验上,一方面我想说的是<strong>自己在 YubiKey 上设置的 PIN 一定要牢记</strong><del>(关于我忘记了自己两个月前为 YubiKey 的 OpenPGP 功能设置的 PIN 这件事(还好之前备份过私钥文件,重新导入回来了))</del>,另一方面就是要把 YubiKey 放在一个你认为合适的位置(要兼顾安全性和方便性),当然更不要随便乱放导致弄丢。</p>
<h2>游戏</h2>
<h3>原神</h3>
<p><img src="./images/2023-5-genshin-impact.png" alt="2023-5-genshine-impact.png"></p>
<p>原神是我四月份刚刚开始玩的,玩到现在玩了差不多一个多月吧。感觉还是挺好玩的,在我玩过的游戏当中算是比较好玩的那一类。</p>
<p>游戏的类型是 RPG,我玩过的 RPG 不多,但是感觉原神和我记忆里的 RPG 游戏从玩法上差不多(有主线剧情任务(魔神任务)和支线任务)。单从任务系统的设计上来说,我还挺喜欢它的角色支线任务的设置(好像叫传说任务),这种任务的玩法有点类似 Galgame 和 RPG 的结合体,有多结局和流程图以及结局的 CG,也可以重复进行从而达成所有结局,我目前玩了芭芭拉的角色支线任务,感觉和 Galgame 差不多。如果角色和剧情多一点就好了。其它支线任务有一个缺点就是接了之后不能取消,我最近才发现,结果就导致主线剧情有时会因为支线任务没做完而无法继续。我玩到现在主线和支线的剧情感觉还是不错的,有些剧情还是挺有趣的,也有的剧情挺感人的。</p>
<p>每日委托实在有点太千篇一律了,我不是很喜欢,以至于有的时候我即使登录游戏也懒得做每日委托。</p>
<p>从战斗系统上来说,其实这方面我玩的不怎么样。原因之一是因为我不氪金,所以抽到的角色和武器也不多,而在我抽到的角色中符合我审美的就更少了。而我又很希望一个队伍里都是我所喜欢的角色,所以队伍搭配和战斗能力可能都不是很好。</p>
<p>下个版本有一个新的猫耳娘角色(绮良良),感觉还挺好看的,虽然没有到特别特别喜欢的程度,但还是希望到时候可以抽到。</p>
<h3>雀魂麻将</h3>
<p>虽然已经了解玩法并入门,但是我大概不会继续玩下去,原因是这个麻将一局的时间感觉不太确定,有时候会拖到很久,太占时间了。</p>
<h3>Minecraft</h3>
<p>这几个月里确实玩的不多,除了 23w13a_or_b 那个快照更新的时候重新玩了一下。</p>
<h2>DN42</h2>
<p>在学习 DN42 的过程中学习了 WireGuard 和 BIRD2 等软件的使用,过程中踩了不少坑,感谢 <a href="https://i.apeiria.net/">Misaka13514</a> 孜孜不倦的指导。</p>
<p>P.S. 我注册 DN42 域名的时候竟然发现 <a href="https://neko.dn42/">neko.dn42</a> 这个域名还没有被注册于是直接注册了,成功拿到,好耶!</p>
<p><img src="./images/2023-5-dn42-domain-register.png" alt="2023-5-dn42-domain-register.png"></p>
<h2>CTF</h2>
<p>今年三月份的时候参加了一场线下的 CTF。这次比赛组队是每个学校组几个队,而我很幸运地和两个大佬组在了一队,我就做了个签到题和一道最简单的 Misc 题。</p>
<p>参加之前低估了这次比赛的难度,本来还是有机会多做出一道 Pwn 的题目的,可惜解法我是在最后的时间想到的,以至于没有时间进行验证和作答了。</p>
<p>虽然最后还是拿奖了,不过 CTF 的分确实不高。</p>
<h2>AI 相关</h2>
<h3>GitHub Copilot</h3>
<p>前几天向一个同学问了一下我学校的 edu 邮箱是怎么登录的,然后用这个 edu 邮箱去申请了一下 GitHub 的 Student Pack。这个 Student Pack 里我最用得着的其实就是 GitHub Copilot,别的我到目前为止还没怎么用。</p>
<p>我此前用过 Tabnine 的免费版和 AWS 的 CodeWhisperer。前者好像是只能单行补全的,但是因为我很长一段时间里没有找到它的替代品(虽然那段时间 Copilot 已经出了但是我那时候还没整明白学校的 edu 邮箱怎么登录)所以忍受着 Tabnine 免费版用了很久,也不好用。后来四月份的时候 AWS 出了 Copilot 的竞品 CodeWhisperer,凑合着一直用着,效果也还行。直到前两天终于换上了 GitHub Copilot,感觉不管是上下文的理解还是补全的正确性都比 Tabnine 免费版和 CodeWhisperer 强了一大截。</p>
<h3>New Bing</h3>
<p>New Bing 刚开始用的时候感觉还是挺新鲜的,但是我目前还是手动用 Google 搜索东西更多一点。</p>
<h3>大语言模型</h3>
<p>某一天刷知乎的时候这篇文章吸引了我的注意:</p>
<ul>
<li><a href="https://zhuanlan.zhihu.com/p/623648932">rwkv.cpp: CPU 也能跑的 RNN 中文语言大模型</a></li>
</ul>
<p>反正也没什么事,就随便玩了玩。用的是 <a href="https://huggingface.co/BlinkDL/rwkv-4-raven/tree/main">这个</a> 里面的一个模型。虽然效果明显远远不如 ChatGPT,不过比我一开始想象的还是要好一点,<del>还有最重要的是可以扮猫娘</del>。</p>
<p>同时在使用中发现了一个小问题,于是顺便开了个 PR 解决了。</p>
<ul>
<li><a href="https://github.com/saharNooby/rwkv.cpp/pull/58">Fix encoding issue when loading prompt data by baiyuanneko · Pull Request #58 · saharNooby/rwkv.cpp</a></li>
</ul>
<h2>计算机相关</h2>
<p>计算机语言方面,试着学习了 C#,并写了一个小项目来练手(虽然没完工)。目前还学的不是很深入,不过在这个过程中对面向对象程序设计有了更深入的理解。</p>
<h2>动画和 Galgame</h2>
<p>从三月份以来其实没怎么玩过 Galgame。除了五月份的时候玩了个<a href="https://store.steampowered.com/app/2114770/">《零岁的星光》</a>,最近我是在推<a href="https://bgm.tv/subject/409483">《天使☆騒々 RE-BOOT!》</a>。</p>
<p>三月份以及从今年一月份到三月份之前中我推过的 Galgame 中比较喜欢的是<a href="https://bgm.tv/subject/22423">《樱之诗》</a>和<a href="https://bgm.tv/subject/257527">《青空下的加缪》</a>。</p>
<p>喜欢《樱之诗》的原因是觉得各个方面都挺不错的。和很多人相反,我其实是挺喜欢《樱之诗》的这种描绘日常的风格的,觉得有一种闲适自然(?)的感觉,当然每个人对作品的品味都不一样。游戏的整个气氛也挺美的,玩着很放松很舒服。音乐/BGM当中也有几首让我觉得很好听,也单曲循环了一阵子。下面列举几首:(链接均指向网易云音乐)</p>
<ul>
<li><a href="https://music.163.com/song?id=399367366">舞い上がる因果交流のひかり</a></li>
<li><a href="https://music.163.com/song?id=399367367">瞬間を閉じ込めた永遠</a></li>
<li><a href="https://music.163.com/song?id=399366411">螺旋に伸びる色彩</a></li>
</ul>
<p><warning-hint>这里提示一下在 Linux 下想要用 Wine 来运行《樱之诗》的人:这个游戏用原版 Wine 运行的话在某一段剧情会卡死崩溃。如果遇到了可以换用 Proton 运行时来运行。</warning-hint></p>
<p>喜欢《青空下的加缪》一方面是因为气氛很美,作画也很精美,另一方面剧情至少我觉得很感人。结局有点遗憾,不过从整个故事的角度来看也是一种必然。</p>
<p>动画方面就挑几部我这几个月看的又很喜欢的动画来评价吧。</p>
<p>首先是《虚构推理》。Bangumi 上这部的评分好像不是很高,但是我自己还是很喜欢的:我觉得这部番的女主(<a href="https://bgm.tv/character/66614">岩永琴子</a>)很好看,在性格上又很有特点;这部番的整体画风还不错;故事背景、具体情节和故事主题从整体来讲都比较对我的胃口。这部番和《冰菓》给我的感觉不太相同,但我觉得它比《冰菓》还要更有意思一些(也许一个原因是我对千反田爱榴完全无感,但是这部番的女主还挺有趣的)。</p>
<p>其次是《天使降临到我身边》,OP 和 ED 都很好听!(不过我更喜欢 ED 一点)</p>
<p>目前我正在看的动画是<a href="https://bgm.tv/subject/386809">《【推しの子】》</a>和<a href="https://bgm.tv/subject/909">《とらドラ!》</a>。</p>
]]>
</content>
<summary type="html">关于在没写博客的这五个月里我都干了些什么</summary>
<updated>2023-05-20T00:00:00.000Z</updated>
<published>2023-05-20T00:00:00.000Z</published>
</entry><entry>
<title>关于我的 2022</title>
<link href="https://nekomoe.xyz/index.html?type=article&filename=my-2022.md"/>
<id>https://nekomoe.xyz/index.html?type=article&filename=my-2022.md</id>
<content type="html">
<![CDATA[
<p>2022 年里我也是顺利从高中毕业,迈入大学生活了。因为疫情原因,从2022年3月开始大概两个半月的时间我们高中都是以网课的形式在上课,感谢这段时间里香子兰群里的大家在 Minecraft 里的陪伴,让我在这段枯燥的学习过程之余多了许多愉快的时光。也感谢 <a href="https://i.apeiria.net/">Misaka13514</a>、<a href="https://koishi514.ml/">scientificworld</a>、<a href="https://mzwing.eu.org/">mzwing</a>、<a href="https://im.chihuo2104.dev/">chihuo2104</a> 以及其它很多人对我的高考祝福。</p>
<p>高考之后的这个暑假我大概没有做很多事情,虽然回想起来的话好像确实是做了一个<a href="https://github.com/baiyuanneko/my-bot">能在群聊中使用的 bot</a> 和一个<a href="https://nekomoe.xyz/index.html?type=article&filename=minecraft-chat.md">能从 Minecraft 日志中导出聊天记录的程序</a>,但是我其实真正花在代码上的时间并不多,还是有点无聊的。这里还是感谢这段时间里香子兰群里的大家在 Minecraft 中的陪伴(特别是 <a href="https://i.apeiria.net/">Misaka13514</a> 和 <a href="https://koishi514.ml/">scientificworld</a>),让我消磨了无聊的时间。</p>
<p>大学里开了一门教 Python 的课,在这门课中我算是基本掌握了 Python 的语法,然后我在大概是接近年底的时候用Python 3(Pypy 3)稍微刷了一些洛谷上的题目,大致学会了深度优先搜索的算法和一些常见的剪枝方法,也了解了一些算法思想(比如“贪心算法”)。动态规划看着就好难所以没什么兴趣学习。目前也还没有学习 C++,因为暂时感觉对我目前用处不大,也许在之后时间比较有空并且我比较有兴趣的时候会继续学习 C++。</p>
<p>这一年里我第一次参加了 <a href="https://hack.lug.ustc.edu.cn/">Hackergame</a>,感谢 <a href="https://i.apeiria.net/">Misaka13514</a> 介绍给我这个比赛,比赛的过程很开心!值得一提的是,比赛中有一道叫做“二次元神经网络”的题目的题目背景中出现了 NovelAI。大概现在正在看这篇文章的人应该都已经听说过 NovelAI 或者 ChatGPT 了?嘛,其实相比 ChatGPT 来说还是 NovelAI 给我的震撼程度更大一点。我以前用过一些二次元插画生成的程序或者网站(比如 <a href="https://crypko.ai/">Crypko AI</a> 以及 <a href="https://make.girls.moe/">MakeGirlsMoe</a>),但是效果不是特别好。但是 NovelAI 生成的插画就非常的精美好看。相比之下 ChatGPT 虽然似乎更厉害,但是给我感觉还是有点傻傻的,没有 NovelAI 给我的那种惊喜感。很可惜的是我的电脑没有独显,没有办法在自己的电脑上实践 AI 绘画。</p>
<p>最近几个月里一直在玩 Galgame,我感觉我在过去几个月里玩过的 Galgame 数量快要和我在这几年里玩过的 Galgame 总数要差不多了。虽然我接触 Galgame 大概是从初中开始的,但是在高中里却玩的不多。其实我的高中又不是寄宿制学校,也就是说我完全可以在放学回家之后的时间玩,但是如果用本来应该用来写作业的几个小时时间去玩 Galgame,还是会让我的内心有一点点负罪感的,虽然这段时间里本来我其实也没在好好写作业就是了。</p>
<p>这几个月里我遇到了很多自己比较喜欢的 Galgame,比如《冬滚滚》和《星空列车与白的旅行》。关于前者,其实一开始吸引我的是它好看的封面,于是详细了解了一下,是科幻+悬疑类型,是我比较感兴趣的题材,于是就简单地决定下载开玩了。真正去玩的时候发现自己很喜欢里面的角色 空丘夕阳。其实对我来说,如果一个 ACG 作品能找出一个我很喜欢的角色,我觉得是一件很幸运的事情,毕竟许多作品中连一个自己很喜欢的角色都找不到。这也是我喜欢《星空列车与白的旅行》的原因之一,我很喜欢里面的角色诺瓦。《星空列车与白的旅行》是 <a href="https://i.apeiria.net/">Misaka13514</a> 推荐给我的,许多人说这部作品比较“像童话”,它的故事前期还是比较温馨的,但是其实它最终要讲述的道理大概可以算是深刻的,我觉得还是一部比较让人感动的、剧情也不错的作品,还是很推荐没有玩过的人去尝试一下的。</p>
<p>在玩过《冬滚滚》之后,<a href="https://koishi514.ml/">scientificworld</a> 也向我推荐了一些类似风格的作品,打算之后去玩玩看。</p>
<p>这一年里我能感受到许多人对我的关心,谢谢你们。希望自己在新的一年里能继续加油。祝大家新年快乐!</p>
<h3>See also</h3>
<p>也可以看看其它人的年终总结:</p>
<ul>
<li><a href="https://blog.apeiria.net/2022/my-2022/">我的 2022</a> by <a href="https://i.apeiria.net/">Misaka13514</a></li>
<li><a href="https://koishi514.ml/blog/bbg/index.html?type=article&filename=tfdCB7WihRBr.md">我的 2022 年终总结</a> by <a href="https://koishi514.ml/">scientificworld</a></li>
<li><a href="https://blog.chihuo2104.dev/posts/goodbye-2022">2022 年度总结</a> by <a href="https://im.chihuo2104.dev/">chihuo2104</a></li>
<li><a href="https://blog.mk1.io/posts/2023">2023新年快乐! + 年度总结</a> by <a href="https://blog.mk1.io/pages/about">Ray</a></li>
<li><a href="https://mujitogawa.github.io/posts/b7a38911/">2022总结&amp;My Resolutions</a> by <a href="https://mujitogawa.github.io/about/">Muji Togawa</a></li>
</ul>
]]>
</content>
<summary type="html">关于我的 2022</summary>
<updated>2023-01-10T00:00:00.000Z</updated>
<published>2023-01-10T00:00:00.000Z</published>
</entry><entry>
<title>参加 Hackergame 2022 的一些感想和部分题解</title>
<link href="https://nekomoe.xyz/index.html?type=article&filename=hackergame-2022.md"/>
<id>https://nekomoe.xyz/index.html?type=article&filename=hackergame-2022.md</id>
<content type="html">
<![CDATA[
<p>参加了中国科学技术大学第九届信息安全大赛(即 Hackergame 2022),最终总分 4600,排名 64,排名有点遗憾地没有达到前 50,不过本来我的 CTF 经验也很少,而且,比赛的过程还是很开心的,也学到了很多很有意思的知识。</p>
<p><info-hint>Hackergame 2022 比赛已于 2022 年 10 月 29 日中午 12:00 正式结束,本文发布于当天晚上,不存在提前公布题解的情况。</info-hint></p>
<h1>一些感想</h1>
<p>这次给我印象很深的是「杯窗鹅影」和「传达不到的文件」这两题,一个是因为自己平时用 Wine 也挺多的,但是却很少思考 Wine 存在的安全问题。包括我在网上搜集「Wine 如何访问 Linux 根目录」相关资料的时候,也发现很多人其实都是认为删除了 Wine 默认的 <code>Z:</code> 磁盘映射之后,Wine 运行的程序就无法访问 Linux 根目录了(我自己在做这道题之前也是这样想的);再比如「传达不到的文件」这一题,即使设置了文件权限,也不代表文件完全处于保护之中,所以这两道题给我的启示就是说计算机安全是没有也不能「想当然」的。可惜的是「传达不到的文件」这题我在比赛时未能想到解题方法。</p>
<p>另外一个给我印象很深的是「evilCallback」这题,V8 是 Chromium 所使用的 JavaScript 引擎,使用的是非常广泛的,并且它的开发者的水平都是非常顶尖的,但是这样的程序也并不是完美的,而计算机安全中任何一个小小的漏洞和疏忽(即使漏洞想要被利用非常困难,但也是有可能的)都有可能导致不良的后果。所以说计算机安全真的是一个很深奥的学问。当然,以我的水平是无法利用「evilCallback」的漏洞的((</p>
<h1>部分题目的题解</h1>
<h2>Flag 自动机</h2>
<p>因为之前没有反汇编相关的经验,所以在这道题上卡了很久。不过事后来看还是不难的。不过因为这是我第一次学习和实践反汇编相关的知识,还取得了成功,所以感觉这道题对我自己还是很有意义的。</p>
<p>首先是「获取 Flag」的按钮在鼠标移上去之后会不断乱动;其次是,即使成功点击了「获取 Flag」的按钮,程序会弹出「你不是本机的超级管理员」的提示,也不会输出 Flag。</p>
<hr>
<p>我首先是用 IDA 看了很久的汇编代码执行流程,然后发现看不太懂。同时,在反汇编后可以注意到程序中包含这样一个字符串:<code>Hint: You don&#39;t need to reverse the encryption itself.</code>,因此我开始尝试通过修改程序的汇编代码的执行流程来获取 Flag,而不是研究程序的汇编代码执行流程。</p>
<p>因为此前没有反汇编和修改汇编代码的相关经验,所以一开始我并不知道使用什么软件来修改程序的汇编代码。我一开始尝试了原版 Ollydbg 和 Windbg,可是都没怎么用明白。最后换用 LCG 版本的 Ollydbg 来修改汇编代码。我主要使用了两个功能:LCG 版本的 Ollydbg 自带的参考文本字串检索功能,以及分析代码功能,可以让我了解从某个位置到另一位置,中间包含的汇编代码大致都在干些什么。</p>
<p>对于「按钮乱动」的问题,首先搜索字符串「放手离开」,可以找到「获取 Flag 按钮」和「退出程序按钮」这两个按钮的创建流程。考虑到<code>0040159F</code>上面的汇编代码看上去像是用来创建「获取 Flag」的按钮,那么也许删去接下来的一些汇编代码就可以解决按钮会乱动的问题。于是凭感觉使用 Ollydbg 将 <code>0040159F</code>到 <code>004015FB</code> 的汇编代码全部改为 <code>nop</code>,获取 Flag 的按钮就不会乱动了。</p>
<p>对于「无法输出 Flag」的问题,搜索文本「您不是本机的超级管理员」,在这个文本下面的<code>0040183B</code>处可以看到一处<code>jmp</code>指令,而这个<code>jmp</code>指令的下一行就是正常输出 Flag 相关的流程,所以删除掉这处<code>jmp</code>即可让程序正常输出 Flag。不过,也可以将<code>004017FD</code>到<code>0040183B</code>的汇编代码全部改为<code>nop</code>,这样那个「您不是本机的超级管理员」的对话框也不会弹出了(不过如果嫌麻烦,只删<code>0040183B</code>处的这个<code>jmp</code>指令应该也可以)。</p>
<hr>
<p>这道题让我学习了反汇编和修改汇编代码的基本知识,虽然做的时候花了很久的时间,但是事后来看其实是不难的,我花的时间太久,主要还是因为我的经验不足而且操作不熟练。</p>
<h2>微积分计算小练习</h2>
<p>这道题主要是需要注意到<code>姓名</code>字段的内容在“练习结果”页面不经任何处理就被加载到结果页面中。而检测练习结果的程序又会在加载结果页面的时候设置会话 Cookie 为 Flag,然后才会输出分数等信息。因此我们可以考虑使用对姓名字段进行一个类似于<code>XSS</code>注入的过程,在结果页面加载姓名字段的时候想办法把 Cookie 中的内容放到分数输出里面。然后把得到的结果页面 URL 放到检测练习结果的程序里面,让检测练习结果的程序输出分数的时候输出 Flag。(这段话说起来好像有些绕((</p>
<p>我使用的是 <code>&lt;img&gt;</code> 标签的 <code>onerror</code> 属性进行 XSS。在姓名字段填入如下内容,然后提交后,将结果页面的 URL 提交到那个检测练习结果的程序里即可:</p>
<pre><code>&lt;img src=&quot;./neko.png&quot; onerror=&quot;document.querySelector(`#score`).innerHTML=document.cookie&quot; /&gt;
</code></pre>
<h2>杯窗鹅影</h2>
<p>我参考的文章:<a href="https://schlafwandler.github.io/posts/attacking-wine-part-i/">Attacking applications running under WINE (Part I)</a>。</p>
<p>从这篇文章中可以知道,我们可以通过 Wine 运行 Linux Shellcode,并且上面的参考文章中已经给出了示例代码,所以直接使用 <a href="https://github.com/schlafwandler/attacking_wine/blob/master/Part_I/exec_shellcode.c">https://github.com/schlafwandler/attacking_wine/blob/master/Part_I/exec_shellcode.c</a> 这里面的代码,将<code>linux32_printline</code>中的 Shellcode 替换成读取<code>/flag1</code>或<code>/flag2</code>的Shellcode然后编译,将编译得到的程序提交即可。</p>
<p>关于如何生成一段 Shellcode,我的方法是,先用 <a href="https://www.exploit-db.com/exploits/44445">https://www.exploit-db.com/exploits/44445</a> 这个 Python2 脚本将 Shell 命令转换成汇编代码,然后使用如下命令生成二进制文件:</p>
<pre><code>nasm -f elf64 shell.asm -o shell.o
ld shell.o -o shell
./shell
</code></pre>
<p>最后用 <a href="https://github.com/tangsilian/SomeCode/tree/master/bin2shellcode">https://github.com/tangsilian/SomeCode/tree/master/bin2shellcode</a> 将二进制文件转换成 ShellCode。</p>
<h2>量子藏宝图</h2>
<p><strong>第一步:登录进入题目</strong></p>
<p>首先是要知道 <code>BB84</code> 加密算法是什么工作的,这里我参考的是<a href="https://zhuanlan.zhihu.com/p/22474140">https://zhuanlan.zhihu.com/p/22474140</a> 这篇知乎文章。</p>
<p>我的做法是:制备基底填写30个字母x(或者全填加号),量子态填写30个0(或者全填1),然后看它给的测量基底和你填写的制备基底有多少个字符是相同的,就填写多少个量子态那里填写的数字,比如如果量子态全填的0,然后测量基底和制备基底有11个字符相同的话,最后那个安全密钥就填写11个数字0。</p>
<p>我的制备基底和量子态的长度都是30个字符长度,因为如果这里的字符长度太小的话,最后它给的测量基底和你填写的制备基底可能只有小于10个字符是相同的,但是那个安全密钥长度应该是必须大于10个字符长度,所以制备基底和量子态的长度不能太短。</p>
<p><strong>第二步:解出「量子电路图」</strong></p>
<p>一开始查了一堆乱七八糟的资料试图理解量子电路 <del>,但是可惜的是我实在看不懂</del>,不过考虑到题目给了我一个 QISKit 的链接,而 QISKit 又是一个可以基于 Python 的可以用来模拟量子电路的框架,那么,我们能不能使用 QISKit 来模拟它给出的电路图呢?答案是<strong>可以</strong>的。</p>
<p>先导入 QISKit 和其它的一些库,并搭建电路:</p>
<pre><code>from qiskit import QuantumCircuit, Aer, execute, transpile;
circ = QuantumCircuit(129,128);
</code></pre>
<p>然后就是漫长的电路搭建过程,这里给出几行例子:</p>
<pre><code>circ.x(128); # 在 q128 处放置那个写着 X 的小方块
circ.h(128); # 在 q128 处放置那个写着 H 的小方块
circ.barrier(); # 创建那个带有灰色背景的竖线虚线,虽然我也不知道那个竖线叫什么()
circ.cx(0,128); # 在 q0 处创建那个蓝色的最底下有加号的竖线,当然,我也不知道这个竖线叫什么()
</code></pre>
<p>电路布置完成后,放置测量器:(至于为什么要放测量器,我参考的是这篇文章:<a href="https://www.qmunity.tech/tutorials/bernstein-vazirani-algorithm">https://www.qmunity.tech/tutorials/bernstein-vazirani-algorithm</a> )</p>
<pre><code>for i in range(128):
circ.measure(i, i)
</code></pre>
<p>最后编写模拟电路并输出 Flag 的相关代码;</p>
<pre><code>sim = Aer.get_backend(&quot;aer_simulator&quot;);
result = sim.run(circ).result().get_counts();
print(result);
</code></pre>
<p>注意最后会得到的是二进制的 Flag,按 ASCII 编码进行解读即可得到 Flag。</p>
<h2>火眼金睛的小 E</h2>
<p>第一问就是简简单单的 BinDiff,然后就是第二问了。由于并没有找到使用命令行进行 Bindiff 的方法,也没有找到 Bindiff 有什么其它的接口,因此我只能手动进行 Bindiff 了。因为输入相同的时间戳可以得到相同的题目,因此我们可以先随意填写100次,从而一次获取全部的题目,然后慢慢 Bindiff。时间限制是 60 分钟,这 60 分钟里要完成获取题目、下载需要比较的文件、进行 100 次 BinDiff、提交所有 BinDiff 结果这四个步骤,所以肯定是有一些过程需要自动化的。实际上除了进行 BinDiff 这一步骤之外,获取所有比较题目、提交所有比较结果、每次下载两个bin文件的过程都是可以自动化的, <del>最后练一下手速,争取做到每分钟两次bindiff,基本上就够了。 一开始操作不熟练,再加上很多过程一开始没有自动化,所以浪费了好几个小时。</del> (别学我浪费时间手动 Bindiff(</p>
<h1>官方题解的一些观后感</h1>
<h2>链上记忆大师</h2>
<p>我大概确实是没有完成这道题所需的相关的基础知识,实在想不到 <code>Player Bytecode</code> 指的是 <code>Solidity</code> 语言源代码编译之后的字节码qwq</p>
<h2>二次元神经网络</h2>
<p>明明已经想到是利用 Pickle 的反序列化漏洞,也构造出写入那10张图像到 <code>result.json</code> 的 payload 了,可惜最后仍然没能做出来。对比官方题解,我的问题出在两个地方。</p>
<p>一个是我需要用<code>torch.save()</code>来保存<code>Exploit()</code>而不能手动写入,否则会提示「在加载和解析模型时出错」。另外一个是我疏忽大意,本来应该将<code>result.json</code>保存到<code>/tmp/</code>的,但我为了在本地调试方便直接将<code>result.json</code>保存到了<code>infer.py</code>所在目录。</p>
<p>我的失败的代码如下:(由于篇幅原因,图像的 base64 在此省略)</p>
<pre><code>import pickle
import os
class Exploit(object):
def __reduce__(self):
return (eval, (&#39;&#39;&#39;open(&quot;./result.json&quot;, &quot;wb&quot;).write(b&#39;{&quot;gen_imgs_b64&quot;:[]}&#39;)&#39;&#39;&#39;,),);
attack = Exploit();
attack_payload = pickle.dumps(attack,protocol=0);
attack_payload = attack_payload.decode(&#39;UTF-8&#39;).strip(&quot;.&quot;).encode();
with open(&quot;./checkpoint/model.pt&quot;, &quot;wb&quot;) as handler:
handler.write(attack_payload+b&#39;c__builtin__\neval\np0\n(Vsys.exit()\np1\ntp2\nRp3\n.&#39;);
</code></pre>
<h1>总结</h1>
<p>虽然没有参加过几次 CTF,但是这次 Hackergame 给我感觉还是很有意思的,除了排名止步于 64 名有点遗憾(本来感觉就差一点点就可以排到前 50 名的qwq),明年也许会继续参加。</p>
<h1>另请参见</h1>
<ul>
<li><a href="https://blog.apeiria.net/2022/hackergame-2022-writeup/">Hackergame 2022 WriteUp</a> by <a href="https://apeiria.net/">欠陥電気</a></li>
</ul>
]]>
</content>
<summary type="html">参加了中国科学技术大学第九届信息安全大赛(即 Hackergame 2022),最终总分 4600,排名 64,排名有点遗憾地没有达到前 50,不过本来我的 CTF 经验也很少,而且,比赛的过程还是很开心的,也学到了很多很有意思的知识。</summary>
<updated>2022-10-29T12:00:00.000Z</updated>
<published>2022-10-29T12:00:00.000Z</published>
</entry><entry>
<title>Python 和 Node.js 实现 Minecraft 聊天记录导出和检索程序</title>
<link href="https://nekomoe.xyz/index.html?type=article&filename=minecraft-chat.md"/>
<id>https://nekomoe.xyz/index.html?type=article&filename=minecraft-chat.md</id>
<content type="html">
<![CDATA[
<p>在 Minecraft 中聊天是一件趣事,然而过往的聊天记录,却无法简单地回顾。</p>
<p>我们怎样才能找回过往在 Minecraft 中留下的点点滴滴的回忆呢?</p>
<p><info-hint>本文中,导出的聊天记录是<strong>临时所用的中间格式</strong>。只有在通过本文第二部分的检索程序实现才能够从导出的聊天记录中检索消息。</info-hint></p>
<h2>特别感谢</h2>
<p>本文中的数据检索相关功能主要是由 <strong><a href="https://github.com/Misaka13514/">欠陥電気 𝑹𝒂𝒅𝒊𝒐𝑵𝒐𝒊𝒔𝒆</a></strong> 开发和完善。</p>
<h2>准备工作</h2>
<ul>
<li>Minecraft 客户端日志</li>
<li>Node.js</li>
<li>Python 3</li>
</ul>
<blockquote>
<p>小提示:Minecraft 日志存放于<code>.minecraft/logs</code>(若开启版本隔离,则是<code>.minecraft/&lt;版本&gt;/logs</code>)</p>
</blockquote>
<h3>简单分析日志格式</h3>
<p>随便打开任何一个带有聊天记录的日志,定位到日志中带有聊天记录的行:</p>
<pre><code>[20:00:00] [INFO]: &lt;baiyuanneko&gt; hi
</code></pre>
<p>容易发现格式如下:</p>
<pre><code>[当天时间(不包含当天日期)] [无关信息][]: &lt;消息发送者&gt; 消息内容
</code></pre>
<p>我们下一步要进行的是聊天日志的导出,因此必要的信息是消息发送时间、发送者和消息内容。</p>
<p>只考虑这一行聊天记录:</p>
<ul>
<li>消息发送者通过<strong>正则表达式</strong>匹配<strong>尖括号</strong>可以得到</li>
<li>消息发送时间通过<strong>正则表达式</strong>匹配<strong>第一个方括号</strong>可以得到,但是只是当天内的时间却不包含当天日期,不过容易发现当天日期可以通过<strong>读取日志的文件名</strong>变相获取。</li>
<li>消息内容的获取方式很多,可以将方括号、尖括号、冒号全部去除,剩下的就是具体内容;也可以读取尖括号之后的内容来得到消息内容。</li>
</ul>
<p>最后我们只剩下需要解决的问题就是:</p>
<ul>
<li>显然,日志中不是每一行都包含聊天记录,那么如何过滤只得到聊天记录行?</li>
</ul>
<p>这个问题比想象中简单,因为经过大量观察和实验可以发现,只要一行中包含<strong>尖括号</strong>(即通过正则表达式匹配到了尖括号),我们基本可以断言这一行就是包含聊天记录的行。</p>
<blockquote>
<p>小提示:实际上还有一个例外,就是有时命令的帮助文本中也会出现尖括号,不过一方面,命令的帮助文本的出现概率通常远小于聊天记录的内容数量,从而你可以事后或在处理过程中手动筛选,如果你去做了就会发现,<strong>这个工作量肯定会比你想象中小很多</strong>,另一方面本身你会发现很难简单地判断出聊天记录行和帮助文本行的区别。所以手动筛选应该已经是比较好的解决方案(如果你有更好的,欢迎告诉我,我会更正)。</p>
</blockquote>
<h2>开始实现聊天记录导出</h2>
<blockquote>
<p>本部分的代码使用 Node.js 完成。</p>
</blockquote>
<h3>简单的步骤分解</h3>
<ol>
<li>遍历一个文件夹(假设为<code>source</code>文件夹)中的所有日志文件(此处可以使用<code>fs.readdirsync()</code>实现)</li>
<li>逐一对每个日志文件逐行地读取(如果日志放在压缩包中,则先解压)(此处使用<code>fs.readFileSync()</code>结合正则表达式<code>/\r?\n/</code>匹配得到包含每一行的内容的数组)</li>
<li>若某一行包含尖括号,则判定为聊天日志行(把帮助日志筛选出去的步骤可以加在这里)(附:判断尖括号的正则表达式是<code>/\&lt;(.+?)\&gt;/</code>)</li>
<li>对于聊天日志行,使用正则表达式匹配尖括号,获取消息发送者;使用正则表达式匹配第一个方括号,获取消息当天发送时间,然后结合当前日志的文件名获取当天日期,组合后使用 <code>new Date()</code> 获取具体时间对象,最后使用 <code>Date.parse()</code>获取完整的消息发送的<strong>时间戳</strong>。最后将行中的方括号、尖括号、冒号全部去除,得到聊天的具体内容。最后组合成一个 JSON 对象大致如下:</li>
</ol>
<pre><code>{
&quot;Sender&quot;: &quot;baiyuanneko&quot;,
&quot;Time&quot;: 1661414501000,
&quot;Content&quot;: &quot;Hi&quot;
}
</code></pre>
<ol>
<li>将此对象加入到一个数组当中保存起来(可以使用数组的<code>push</code>方法)</li>
<li>导出到文件(可以使用<code>fs.writeFileSync(JSON.parse())</code>实现)</li>
</ol>