<![CDATA[Tiger's Works]]>https://tiger.work/https://tiger.work/favicon.pngTiger's Workshttps://tiger.work/Ghost 6.1Tue, 17 Mar 2026 18:06:30 GMT60<![CDATA[汗水兑出的轻盈]]>https://tiger.work/han-shui-dui-chu-de-qing-ying/68d6729d0983ac000162e6bbWed, 24 Sep 2025 00:00:00 GMT

肥膘是健康的敌人,也是建模的敌人,也是心态的敌人。有一身膘的人其实是很可怜的,世人说他们尽量做到不要用有色眼光看待,可现实并不如此。我想几乎所有人都知道膘的危害,我也就不多赘述。那为什么人不能移除它呢?答案是,可以,就是通过减肥。

我从2025年3月1日开始进行我的减肥计划,第一天的体重为100公斤(200斤)整。这篇文章起稿于2025年9月23日,当天秤值为78公斤。在这半年的时间里,我成功减掉了22公斤(44斤)的体重,故撰此五千字长文分享一下我的减肥心得与方法,希望能对还在减肥路上的人有所帮助。

由于每个人的体质,体格差异不小,所以每个人的有效减肥方式都有所不同。所以本篇的意图只是分享适合我并在我身上成功的减肥方法,不一定会适合所有人。比如有些朋友不吃饭是真的会饿死,有些朋友可能因为关节原因没办法运动。所以正确食用本文的方式应该是予以参考,并在自己总结出的减肥经验中尝试调整。我会尽量讲一些已经被论文证明过的事实,让这份经验适用于大部分人。


准备工作:严谨与精确

可能是因为自己是学理科的,我想分享的第一个经验是,减肥是一项严谨与精确的事。如果你真的想要很认真的去减脂,那就必须对所有数据都了如指掌,这些数据又可以分为自己身体的数据、食物的数据、健身的数据等。身体的数据能让你设定短期与长期的目标,让减肥不再迷茫。食物和健身的数据能精准的帮你控制热量缺口,和告诉你是否减脂计划在跟着你设计的轨迹走。

汗水兑出的轻盈

DEXA 与 BIA

首先我们来说自己身体的数据,减肥路上最大的智商税就是各种品牌各种型号的体脂秤,我这边建议统统不要买。因为凡是使用Bioelectrical Impedance Analysis 生物电阻抗分析作为体脂、肌肉、或其他数据检测的秤,100%是不准的。这种用电阻抗测试出来的数据会被你当天进食、喝水、运动、出汗等各种活动所影响,不同品牌不公公式的差别也显著。既然我们对自己的要求是严谨而精确的,自然不能允许这种有时候18%有时候22%的事情发生。然而这并不是说你完全不需要一个家用秤,用家用秤监控水分和短期体重波动还是非常重要的,因此我会推荐仅拥有体重测量功能的,误差越小越好。

我认为正确获得身体数据的方式是 Dual-Energy X-ray Absorptiometry 双能 X 射线吸收法检测,简称DEXA。这是一种医学影像检测技术,会从仪器发出不同能量的X光照射身体,计算脂肪、骨骼、肌肉的含量。这种检测方法的误差最大只有2%,属于较高精度的检测。并且提供这种服务的医院或者公司会给你生成一个非常详细的报告,甚至包含了你每个部位的脂肪和肌肉分布,你就能知道你其实最需要减掉脂肪的部分是在哪里。

通常第一次拿到DEXA报告的人会震惊到,我也是如此。我开始减肥的第一次测试,体脂高达31%,因为我从来没有系统性的去扫描过所以完全不知道居然这么高。这也变相说明了一个人可能看起来没有那么胖或者BMI不是很离谱,但体脂依然可以是非常离谱的。特别是我的腹部啤酒肚,脂肪率高达40%,基本上除了器官之外就都是脂肪了..

有人问健身房的那种 Inbody 商业秤如何,我的评价是比家用的要稍微准点,但和医学扫描相比差距还是非常大。虽说DEXA在我们这边检测一次要 $40美金(约合284元),但其实不用很频繁的去测,我比较推荐2个月左右一次,也就是每半年3次每一年6次这样,因为在保证肌肉的情况下减肥,体脂率波动不会特别大的,更加像是一个缓慢的下降过程。一个月或更短测试一次是比较奢侈的,除非你真的对自己要求严格,不想有一丝懈怠。至于这种测试方法对身体造成的辐射,我只能说肥胖对身体造成的伤害更大,所以安心去测吧。

汗水兑出的轻盈

Apple Watch 与 心率

智能手表是减肥最好的帮手之一,我可以很负责任的说健身光靠感觉虽然也能瘦下来或长肌肉,但是绝不是效率最高的方法。我选择并强烈推荐的是Apple Watch,普通款就行。Ultra更适合户外运动而非室内,考虑到其重量与价格我不推荐。如果你平时和我一样不习惯戴首饰的话,买最小号的运动的时候戴就行了,不用去纠结于表带等无关紧要的选择,普通橡胶的就没有任何问题。为了方便,下文所有“手表”指代的都是Apple Watch。

最重要也是最核心的手表功能是心率监测,更确切的说是实时心率。在我看来没有比心率更加重要的减脂因素了,因为几个黄金指标,例如燃脂效率、卡路里消耗、心肺能力,全都和心率密切挂钩。心率也是知晓当前身体状况的最直接指标之一,你可能在跑步的时候听到一首节奏激昂的音乐而瞬间觉得全身充满力量,可心情不会对心率造成特别大的影响,如果你的心跳越变越快,那很可能是身体在说它累了。另一种情况,有时虽然你觉得浑身没力气,但一看心率连100都不到,那大概率说明是懒虫作祟或心理因素,实则身体还有许多没有爆发出的能量。

精确的实时心率能让你在有氧时对机器Level或坡度的动态调整,让你的心肺保持在最佳燃脂区间。这里需要解释一下心率区间的概念,我们把常见的心率划分成五个区域,分别是:Zone 1:恢复区(50–60% HRmax);Zone 2:燃脂区(60–70% HRmax);Zone 3:有氧耐力区(70–80% HRmax);Zone 4:无氧阈值区(80–90% HRmax);Zone 5:极限区(90–100% HRmax)。HRmax的意思是你心跳所能达到的最高值,如果你和我一样二十多岁并且没有疾病,可以用全力冲刺跑到喘不上气的方法测试一下最高心率。我们所说的最佳燃脂区间就是Zone 2,一般为60%-70%的最大心率,这个区间每个人都不太一样,对我来说是142-154跳这个范围。有氧运动时,一定要把心率提高到Zone 2区间,在区间内越高越好。

说到心率也不得不提卡路里,他们两个也是密切相关的兄弟。有氧机器上显示的卡路里消耗其实是非常不准的,有时候能相差一百多卡。那么有朋友可能要问了,只有一百多卡需要那么较真吗?是的,别忘了我们的目标是严谨与精确。卡路里消耗是有氧运动效果的直接指标之一,和减肥效果直接挂钩。那为什么一定需要一个智能手表来帮助监测呢?因为根据经验我发现,自己是绝不可能可以估算的准确的,最大的忽悠因素就是流汗,流汗越多并不代表消耗越大,机器阻力越大也不代表消耗越大,有氧因素控制总消耗的因素太多。但可喜的是,只要是有消耗,就代表着有效果!

一切奇奇怪怪的小功能也能帮助你坚持下去,比如健身App中内置的徽章、健身圆环、朋友之间的竞赛等等。打个比方,某个周天你可能累到没有想要坚持下去的心,但看着前六天三色都被合上的圆环,只能告诉自己为了完美的一周,咬咬牙,现在就开车去健身房。当你减肥成功后,看着一个月满满当当的圆环,心里也会有巨大的成就感。

汗水兑出的轻盈

TDEE 与 营养成分表

热量缺口是每日减脂的效率指标,当每日的消耗大于摄入时即形成热量缺口。请注意,这个缺口需要非常仔细和严谨的控制。绝不是只要无限制增加缺口就会瘦的更快,这其实减肥最危险的误区,因为过高的热量缺口会立刻引发身体代谢的降低从而维持最低生命体征,也就是所谓的平台期。这三个字基本上是减脂人的噩梦,你永远不想要自己进入平台期,因为当你早上起床看到自己虽然昨天辛辛苦苦的制造了巨大的缺口,然而体重却没有任何变化甚至倒退变重。这种现象会直接摧毁心里防线,外加缺口过大导致的脂肪摄入不足而出现的差心情,可能会直接驱使你放弃。

那么每天适合自己的热量缺口是多少呢?这对每个人来说都是不一样的。我个人的方法为,我尽量的控制自己少吃,如果第二天心情变差,那就说明缺口太大,如果第二天训练正常并且觉得可以坚持,那这大概率是一个你可以接受的缺口。我个人的缺口是每天500卡左右,是一个非常健康的热量缺口。如果你每天的热量缺口高于1000卡,那大概率不久之后会遭遇平台期,请务必需要注意。

这里就又要提到DEXA扫描的另一个好处,它会告诉你比较精确的 Resting Metabolic Rate,简称 RMR 静息代谢率。这是你在完全静止、不做任何活动时,身体维持 最基本生命功能(呼吸、心跳、维持体温、细胞运作) 所消耗的能量。而 Total Daily Energy Expenditure 总日能量消耗,简称 TDEE,其很大一部分就是RMR。搞清楚自己的TDEE是非常重要的,因为这个指标决定了你在减脂期可以摄入能量的上限,当你摄入超过上限,就是在增肥了。

根据我的经验我想推荐一个小技巧,TDEE(总消耗) ≈ RMR(静息) + TEF(消化) + NEAT(活动) + EAT(运动)。要保持大约500卡左右的热量缺口时,你每天吃的量应该大约是 RMR + EAT 这样子。因为考虑到现在人不是学习就是搞钱,除了健身房之外基本没有NEAT,就是坐在办公室或者躺在家里,然后TEF又只占很小的一部分,所以这两个加起来基本就是500卡的样子。如果你觉得自己除了健身房真的完全不动,就爱躺在家里当废柴(我就是),那么我会推荐 RMR + EAT * 0.6,进一步控制摄入。如果一天中有EAT消耗,则请务必至少吃到RMR,不然第二天状态真的会非常差。

营养成分表是摄入量的好帮手,这点我需要表扬美国佬,基本超市里所有的单品都标有简单易懂的 Nutrition Facts。这点我就不多讲了,因为大部分人都能够看得懂。比较需要注意的是营养成本表上最明显最大字的卡路里不一定是一整包的卡路里,请一定要看清楚每包的分量,正确的卡路里总数应该是 分量 * 每份卡路里


执行:毅力与恒心

短视频平台上说的30天瘦30公斤这种,基本上就是在唬你,没有必要去浪费时间在这些没有任何意义的假科普上。真正能让你减肥成功的,也是所有人最惧怕和欠缺的,就是毅力和恒心。就算你每天只能够保持10卡的热量缺口(半口可乐),那么坚持9.6年下来也能够瘦掉十斤。开个玩笑,10卡的强度实在是微乎其微了,但如果你能每日坚持300-500卡(少吃一份盖浇饭的米饭),减掉10斤只需要2-3个月,是不是很有诱惑力?我就直接把高峰阶段我减脂的安排计划写出来,一天两练的强度其实是非常大的(因为在放暑假没事情所以对自己比较狠心QWQ)。仅供参考!

汗水兑出的轻盈

有氧运动

目标消耗为每日500卡,从椭圆机、正坐单车中二选一。推荐这两个机器的原因是因为我刚开始减脂时体重基数非常大,如果运动负荷太大会导致关节疼痛。椭圆机和正坐单车都属于是新手都非常容易控制节奏的机器,而节奏又和心率直接挂钩。椭圆机甚至还会动用一些核心力量,给你带来更高的心率,这点在一开始心率上升热身阶段中尤为明显。

讲到热身,我建议各位需要注意的是缓慢的增加强度而不是一上来为了达到心率而快节奏迅速进入状态。这样子虽然能成功让你最快的进入减脂心率区间,但是在后半程你的心率会因为前半程激烈的爬升而不够稳定,导致无法很持久的把心率控制在减脂区间的最右边缘。相反,慢速提升强度虽然起初的5-10分钟达不到减脂区间,但后续在体力耗尽之前都能够持续提供一个稳定的心率。

因此,我推荐一周五次,每日有氧时间为40分钟,其中前10分钟用于热身把心率拉到第二区间,后30分钟为黄金热量缺口制造时间。推荐有氧在每天早上空腹,但不要一起来就去,因为那个时候身体的运动机能还没有完全开启。如果你想要把有氧和重量训练放在一天,那我会建议至少中间间隔6-7个小时,因为有氧的消耗会影响到重量训练的效果。

有人会担心有氧时间这么多会掉肌肉,那我可以很负责任的告诉你,如果你的体脂跟我一开始一样是31%,那可以基本不要想着练肌肉了,因为你就算练大了也是被脂肪包裹着,视觉效果绝对不是你想象的彭于晏的那种效果。与其说担心掉肌肉,不如赶紧多有氧把体脂减下来,先让脂肪停止释放炎症分子,先给自己一个好身体,再考虑练肌肉的事情。有人可能也会说练肌肉能增加代谢能加速减脂,可在我看来你身上的肌肉再多,也没有你看到一个小蛋糕或者甜甜圈屏住不吃的消耗大,饮食控制依然是制造缺口最管用的方法!

汗水兑出的轻盈

重量训练

同样消耗为每日500卡,这个卡路里消耗其实我觉得是非常不准的,因为重量训练并不是一个持续型消耗,所以这边的500卡参考只是手表上显示的数值。这里依然是练5休2,每日一个小时左右时间。请注意做重量训练一定一定一定要热身,不然上了重量或者冲自己极限的时候非常容易拉伤扭伤,恢复时间很长会导致你连续坚持天数断裂。我练5修2的安排是周一胸肩,周二背二头,周三腿臀,周四胸肩,周五背二头。一开始尽量不要安排太多臀腿,因为练腿日真的是噩梦般,消耗非常大。因为本篇的主要关注点在减脂,重量安排不是大头,只需保证每一组动作中能完成至少8下,最佳状态为力竭。

重量训练的技巧等我练成彭于晏那样我再写文章吧,现在的肌肉量依旧是比较差劲的,所以没什么经验。但我知道与其要冲重量,正确的动作和最长的行程才是对肌肉肥大重要的因素。训练伙伴也推荐大家去找一个,这样子组间休息时能互相帮助,特别是推胸这类有一定危险性的动作,有个伙伴比较方便能挑战自己的极限。

汗水兑出的轻盈

饮食和睡眠

我认为减脂的饮食,吃够不超比屏住不吃要难得多。三要素脂肪、蛋白质、碳水的分配,我个人建议是在脂肪30%,碳水30%,蛋白质40%这样,然后总摄入需要低于你的TDEE。吃够并且能变着花样吃真的是一种能力,但好在我在吃的方面没有那么讲究,叫我吃一个月的鸡蛋或者鸡胸肉我并不会觉得腻。有些朋友会忽视了脂肪的重要性,脂肪的摄入直接和激素分泌挂钩,会极大的影响到第二天的心情。这就是为什么和朋友出去吃一顿重庆火锅,这一整周都会非常开心的原因。减脂期间一定要吃够30%的脂肪,最直接的方法就是多放点健康油或者吃脂肪集中的食物例如坚果和花生酱。

我在吃的补品主要是维生素,因为不是很喜欢吃蔬菜,所以要靠一些药片来补上。蛋白粉,蛋白棒,肌酸,谷氨酰胺等补给我这边不予评价,因为可怜的作者对一切浓缩蛋白过敏,一吃身上就是疯狂瘙痒,尤为可怜。至于酸类我也不是很敢补充,因为在开始减肥前体检出了高尿酸血症,所以饮食方面酸类和高嘌呤食物都不是很敢吃QWQ。

比任何补剂都要重要的是睡眠,每天一定要保证八个小时睡眠,因为人在睡眠时间,身体动用脂肪来作为能量消耗的比例是最大的,可以说睡觉就是在减脂。我会推荐尽量把用餐和任何形式的训练都移到离睡眠越远越好,高强度的训练会让神经紧张无法入睡。


汗水兑出的轻盈

写在最后:曾经的你我

如果你有耐心读到这,那我有十足的信心相信你是能够减肥成功的。我能告诉你的是,当你在镜子中看见你的身体因为努力而逐渐开始变化的时候,那种兴奋、喜悦、满足感是难以言表的。你会发现曾经最大码都穿不上的裤子,现在慢慢的能穿上并且甚至可能还需要系皮带。你会发现自己逛街时的身躯轻盈了许多,你弯腰系鞋带时再也不会被肚子上的肉卡住。再回头看看曾经的自己是有多么放纵,你会感谢自己开始减肥,开始自律,开始把自己变得更好。加油!

]]>
<![CDATA[酒浅心深]]>https://tiger.work/jiu-qian-xin-shen/68d672470983ac000162e6afMon, 22 Sep 2025 00:00:00 GMT

2025年9月22日凌晨作

相邻两位的酒杯里装满了酒,他们似乎进入了那种被我俗称为「好爽」的状态。彼时桌对面的已经见了底,手勾肩搭背上七嘴八舌。桌角堆着赢来的空瓶子,余光里是零星炸物、矿泉水和皱成团的纸巾…公主安静的坐在我旁边,打量着喝到一半的牛油果饮发呆。兄弟们盯着屏幕,怕输掉游戏又赔了一瓶烧酒。听上去是个愉快的夜晚吧,以前的我也一定会这么认为的。判若云泥,眼前的小杯中空无一物,盘壁上的葱酱也是蘸薯条剩下的。举杯之时依旧眼睛弯弯,可举起的却是大了好几号的轻羽。欢愉仍在,但对我来说,已经不是当初的模样。

偶尔也会因为拂过的一两个笑话,涓涓一小杯下肚。可奇怪了,萌生不了一点兴奋劲,想到过去的每周,每月,每年的深夜都在做着同样的事,心情却截然不同。是老了吗?对曾经的快感心生倦意,甚至开始抗拒。可我才活了二十二载,我对自己说,但又摸不清是真的在诧异还是找借口。

有一天,酒桌来了三位没有到喝酒年纪的客人。一开始略显羞涩,但几杯下肚后又是截然不同。那天我第一次感受到,那满满溢出的能量感,那种稳定又强大的年轻力量正是我很早就失去了的,尽管只有四年之差。是因为酒桌上的“身份期待”在作祟吗?还是因为“比其年长”的标签无声压在心口…有没有可能是“期待与落差”呢,本幻想与他们打成一片,醉酒畅谈,却能从他们问我的问题和看我的眼神中,读到隔阂。

公主大我一些,好像能理解我的感受。牵着我温柔的说,这只是因为有了责任感。它会促使在放纵前对于后果的思考,所以欢愉被冲淡。然而可悲的是这不是件坏事,反而说明我在成长。宿醉划算吗?来日的工作学习变成了浪费时间。熬夜划算吗?器官在负荷下消化因一时冲动而多喝的毒。可又有谁问过,是否愿意让大脑被理智接管。人活着需要消遣和逃避,焦虑和情绪的自我消化又何尝不是对身体的另一种伤害。

目光投向左右,又看到兄弟们明知道我盏中的透明和他们的不同,也依然并肩享受时光。我理解了,默许彼此必然的稳重或许才是真正的情义。默许消散的疯癫,与现实的复杂接轨。默许自己不再是个少年,默许那点不值得去强迫改变的落差。默许是一种浪漫,而不是一种对消极的妥协。中庸是懂得进退,无为是顺势而为,放下是一种享受世界如是。理解了,这种心平气和的智慧,才是我所追求的。

心明了,焦虑散了。

]]>
<![CDATA[一匹布]]>https://tiger.work/yi-pi-bu/68d671f20983ac000162e6a3Sun, 21 Jan 2024 00:00:00 GMT

24年1月,我花了一周时间刷完了王家卫导演的电视剧《繁花》,品出了90年代老上海人对于着装的细致讲究。第一集阿宝在红帮裁缝处定制西装时,爷叔的那句:"西装第一要看料子,一定要英纺,纯羊毛的。夏天嘛,派力斯、凡立丁;冬天嘛,法兰绒、扎别丁、舍维呢,都要英国花呢的...",勾起了我对衣物材料面料的研究兴趣。却发现,中文搜索中的科普文信息差很大。不仅来源欠真实性,繁杂的专业属于和几乎没有逻辑的整理让外行非常难以理解。本文将以我(外行)学习后的理解出发,以我的角度分享纺织物和面料相关(不仅仅是西装)。

首先重要的是搞懂纺织物之间概念的关系。成衣是由单种和多种纺织物组成,也就是俗称的衣服料子或面料。结构最简单的成衣可以理解成手织毛衣,及一整块特殊形状的面料。但出于现代人对时尚的追求,时装行业里比较难找到类似的衣服。大多数时装都有一定的潮流和版型要求,一整块料子很难同时满足两者,但将不同的布料拼合可以。于是乎如今市面上有大量不同的面料可寻,特定比例的纤维与化学混合给了它们不同的特性,也命上了花里胡哨的名字。

面料制造的根是纤维,分为天然纤维(生物产)和合成纤维(人造)。举个例子,最常见的天然纤维就是所有人都一定听过的棉花。这些纤维在采集后经过一系列生产工艺制成纺纱,纺纱可以由单种纤维组成、也可以是多种混合以获得多种特性或改善其中短处。纺纱是最原始的可直接用到衣服上的线,就是儿时父母在家给裤子打补丁用的同色细线。不同于纤维通常用微米来区分,纱/线或以上的处理后材料的分辨几乎都能用肉眼直接得出结论。比如,纺纱的一个兄弟叫做纺线,基本上就是多股纺纱捏在一起成更粗更硬的形态,也就是外婆手织毛衣用的毛线。

动植物纤维

纤维的抉择直接影响了纺纱的特性和质量,而纺纱的特性和质量更是直接决定了面料的舒适度、样子和价格。在《繁花》爷叔的话中:“纯羊毛的”,自然就是指料子的其中一种纤维种类了。在天然纤维的价格体系中,羊绒通常最为昂贵,它取自山羊绒毛,产量稀少、纤维细腻,被誉为“纤维宝石”。紧随其后的是蚕丝,作为典型的高档纺织原料,光泽柔美、触感顺滑,价格长期保持高位。兔毛因纤维细软、保暖性好,也属于较高价位,尤其是安哥拉兔毛,常用于高档针织品。羊毛的价格则相对适中,虽然优质细羊毛(如美利奴)也不便宜,但整体供应量大,普遍低于羊绒和丝绸。麻类纤维(如亚麻、苎麻)因产量较高、工艺较简单,价格普遍在中低档。最后是,作为全球应用最广、产量最大的天然纤维,市场供应充足,因此价格最低。

棉 / 棉花 Cotton

天然植物纤维,棉布贴身保暖柔和但不易打理。通常棉纤维成分超过95%的面料称为纯棉,这类面料较少是100%全棉制成,混合一些其他纤维能够解决一些短处。棉制面料可机洗或手洗,但必须注意不能用力太大导致变形。晾干时不可暴晒,不然容易变黄和降低使用寿命。

优点:易清洁、可水洗、吸湿、耐热、耐碱、保暖、透气、无味

缺点:易破、缩水、变形、缺乏弹性、怕酸、褪色

麻 / 亚麻 Flax

亚麻减少静电的能力让灰尘难以吸附在面料上,变相保护了过敏皮肤和防止了细菌的滋生。干爽凉快的特性使得这种纤维适合制作枕席、床单、台布等。但由于太高的纤维强度,所以舒适性相对降低且褶皱性差,但却又成就了耐刮耐撕、结实的特性。亚麻也是一种容易着色且不易褪色的面料。麻袋就是使用麻类植物的工艺品,以便宜的价格和高强度闻名。

优点:高耐热性、高强度、吸湿、易干、无静电、凉爽、染色性

缺点:褶皱性差、虫蛀、缩水

羊毛 Wool

又称绵羊绒和绵羊毛,属于高级的天然动物纤维之一。不同品种的羊毛的特征差异较大,价格差异也较大,无法在一起总结优缺点,但纤维约细则越贵和越好。在羊毛面料中,精纺和粗纺选择也会对其有很大的影响,主要体现在舒适度、重量、保温性等。纯羊毛面料具有弹性、足够挺括外加颜色纯正并有自然的光泽,这些特性让精纺后的羊毛成为了西装面料的选择之一。

优点:保温、不易燃、柔滑、不褶皱、柔和

缺点:缩水严重、必须干洗、对洗涤用品要求高、对洗涤水温要求高、起球

蚕丝 Silk

蚕丝面料又称之为真丝面料,具有天下无敌的亲肤性。真丝和冰丝(人造)是两个不同的概念。这种面料的高品质是手一摸和看一眼就能感觉出来的,非常丝滑柔顺且具有珍珠般的光泽,是夏季衣物面料的首选,上身后与其他面料有无法比拟的凉爽舒适。但真丝面料的短处也同样闻名,就是其非常难打理。作为一种动物纤维,真丝面料害怕虫蛀、日晒、高温、还非常容易发霉。真丝的抗皱性较差故需要熨烫,且温度不可高于150度,收藏时需避免挂放。

优点:通风、细腻、无静电、无起球、光泽、亲肤

缺点:昂贵、怕汗、易变色、低耐久、虫蛀、怕日晒、虫蛀、发霉、不能漂白、推荐干洗

羊绒 Cashmere

又称山羊绒、开司米、软黄金,是一种精贵的超高价面料。不同于羊毛出在绵羊身上,羊绒只生在山羊腹部的一小块区域。采集羊绒需要用特质的采集工具一点点薅下来,且产量极少。然而,羊绒的轻薄和极细纤维却相比羊毛有高于8倍的保暖性,但需要防虫剂和苛刻的存放护理。不能挂放否则容易变形,需要叠好装袋。羊绒是网传最高级的面料纤维,甚至据说使用这种面料制成的衣服贴身穿着具有减少疲劳和保健的功效。

优点:柔软、保暖、轻盈、细腻、平整、弹性、尊贵

缺点:不结实、产量低、天价、极容易虫蛀、存放条件苛刻

兔毛 Rabbit hair

兔毛通常指安哥拉兔毛,是一种极为细腻柔软的天然纤维,纤维直径通常只有10~15微米,比普通羊毛更轻盈顺滑,手感如丝般柔和,外观带有自然的蓬松与光泽感。它最大的特点是保暖性极佳,由于纤维中含有大量空气层,能够锁住热量而又不显厚重,因此常被用于制作高档毛衣、围巾、披肩等冬季服饰。安哥拉兔毛的外观华丽、质感轻奢,但同时也存在掉毛、不耐磨、护理要求高等缺点,因此常常与羊毛或其他纤维混纺,以兼顾舒适性和实用性。

优点:光滑、蓬松、暖和、顺直、洁白如雪、晶莹、柔软、价格低

缺点:起球、抱合力差

精纺

在精纺毛料的价格体系中,舍维呢通常最为昂贵,属于高档定制面料;扎别丁次之,虽略低于舍维呢,但依旧定位在高价位区间;凡立丁处于中高档水平,整体价格明显低于前两者;派力斯多为中档,适合大众化的轻薄正装需求;哔叽则价格偏中低档,常见于制服与日常西装;而法兰绒通常最为亲民,整体售价最低。

派力斯 Palace

派力斯是一种轻薄而质感细腻的精纺毛料,通常采用高支纱织造,表面平整光洁,色泽柔和,手感顺滑。它常用于制作春夏季西装或轻便外套,给人以精致、干练的印象。

优点:轻薄、透气性好、外观高雅。

缺点:布料偏薄,不够挺括,耐磨性较差。

凡立丁 Valitin

凡立丁是一种经典的精纺毛织物,质地紧密而富有弹性,手感较柔和,面料表面有隐约的纹理感,适合制作西装与制服。它在外观上给人以含蓄稳重的感觉,常用于商务正装。

优点:质地紧密、外观沉稳、垂感佳。

缺点:透气性较一般,舒适性略逊于轻薄面料。

法兰绒 Flannel

法兰绒是一种在精纺毛料基础上经过拉绒处理的面料,表面有一层短而细的绒毛,质地柔软,保暖性好。它既可做西装,也常用于冬季休闲装,显得温暖而舒适。

优点:手感柔软、保暖性强、外观温和。

缺点:易起毛起球,不够挺括,耐磨性偏差。

扎别丁 Gabardine

扎别丁是一种经纬密度很高的斜纹精纺毛料,布面光洁、纹路清晰,坚牢耐用。它最早用于军装,后来广泛用于西装和风衣,以挺括和耐磨著称。

优点:结实耐磨、抗皱性好、版型挺括。

缺点:透气性较差,手感偏硬,穿着舒适度一般。

舍维呢 Worsted Flannel

舍维呢原本指精纺呢绒,后来多指一种光洁平滑、手感柔软、织造精细的毛料。舍维呢适合制作西装、套裙等正式服饰,外观端庄典雅。

优点:质地细腻、色泽自然、垂感好。

缺点:易起皱,不耐频繁摩擦,保养要求高。

哔叽 Serge

哔叽是一种常见的斜纹毛织物,表面有明显的斜纹纹路,质地中等厚度,结实耐穿。它是制服、西装和裤装的经典用料,常给人端庄、干练的印象。

优点:耐磨结实、外观稳重、价格相对实惠。

缺点:手感偏粗硬,舒适性不如高档精纺,透气性一般。

]]>
<![CDATA[数字江湖的暗号]]>https://tiger.work/shu-zi-jiang-hu-de-an-hao/68d66dd40983ac000162e667Sat, 16 Dec 2023 00:00:00 GMT

数字江湖的暗号

您好,各位。今天,我很高兴为您带来一个题目,名为“SSH的实现及其与类似协议的关系”。在今天的讨论中,我们将深入探讨各种协议,如SSH、Telnet、FTP、SFTP、HTTP和SSL。我的目标是阐明它们之间的差异以及它们被应用的具体场景。如果这些术语对您来说听起来不熟悉或令人困惑,我希望我提供的见解将对您有很大的帮助。

数字江湖的暗号

首先,我想从讨论为什么我选择 SSH 作为这个项目的标题开始。我第一次接触 SSH 是通过我的 ICS33 和 ICS46 教授,他们要求我们使用 UCI Openlab 作为我们课程作业的编译环境。经过几个学期的使用,我对 SSH 的好奇心极大地增长了。然而,SSH 的用途远不止帮助学生完成作业。它是一个强大的远程服务器管理、加密文件传输、用户认证和端口转发工具,被企业和个人广泛使用。根据相关统计数据,由于 SSH 的诸多优势,它在远程操作协议市场上占据了重要份额。早在 2000 年,SSH 用户的数量已经达到了两百万。现在,在 2023 年,这个数字已经增长到几乎无法计数的地步。许多IT部门的员工,尤其是运营部门的员工,可能在他们的日常工作中频繁使用 SSH。

数字江湖的暗号

让我们从介绍一些关于这个协议的基本信息开始。SSH 最初是为 类 Unix 系统 开发的,它的主要目的是远程登录和命令执行。随着时间的推移,使用这个协议的应用程序增多,使其与几乎所有系统兼容。SSH 于 1995 年开发,它默认在 端口 22 上运行。它被设计为替代极不安全的 Telnet。SSH 的加密登录和双向数据加密使远程连接非常安全。即使有人在中间截获数据,没有密钥他们也无法阅读。由于登录过程,包括密码,都是加密的,黑客再也不能通过流量分析来尝试盗取远程服务器密码,因此显著增强了服务器在暴露于公共互联网时的安全性。

数字江湖的暗号

Telnet 一直与 SSH 相比较,主要是因为它的安全级别较低。尽管 SSH 由于其加密过程更复杂且稍慢,但大多数个人和企业用户已经转向使用 SSH。Telnet 于 1969 年开发,那时的计算机网络要简单得多,用户也少得多。因此,Telnet 选择了在网络上使用明文传输,包括用户名和密码。尽管 Telnet 的安全扩展有所发展,但统计数据显示,大多数安装的 Telnet 系统并没有这样做。这意味着真实数据包可以在两个主机之间的任何点被截获,如路由器、交换机和网关。然而,到了 20 世纪 90 年代末,随着网络安全重要性的日益被广泛认识,SSH 应运而生。

数字江湖的暗号

Telnet 的传输过程非常直接。作为基于 TCP/IP 协议 的应用程序,客户端通过与服务器建立握手来启动流程,以确认连接。一旦收到肯定的连接确认,客户端就开始以明文形式传输用户名和密码信息。为了适应不同操作系统中的各种快捷键和基本操作,Telnet 在连接建立后的任何传输发生之前,将客户端的动作转换为 NVT(网络虚拟终端)数据包,包括用户名和密码。这些未加密的数据包一旦被服务器接收并确认,便被转换为正确的命令格式进行执行。这些命令的结果随后被封装回 NVT,并发送给客户端,完成一个命令周期。当客户端希望断开连接时,它会向服务器发送相应的命令,从而结束传输过程。

数字江湖的暗号

相比之下,SSH 比 Telnet 复杂得多。然而,由于 SSH 也是基于 TCP/IP 协议建立连接的,因此传输的第一步始终是握手和建立连接。由于幻灯片尺寸限制,TCP 握手过程被简化为仅两个箭头,尽管实际过程更为详细。在握手之后,传输前的过程真正反映了 SSH 在安全性方面的进步。第二步涉及版本协商。目前,主流的SSH 有两个版本:SSH 2.0,代表第二代,以及 SSH 1.X,代表第二代之前的所有版本。两代之间的主要区别是增加了几个扩展协议,包括稍后在本演示中提到的端口转发和隧道技术。SSH 2.0 还引入了新的加密和解密算法,增强了 SSH 的整体设计结构,并重新设计了认证系统。回到我们的主题,在双方同意版本之后,第三步是算法协商。客户端和服务器彼此发送他们支持的算法,并就一个共同的算法过程达成一致,主要是为了接下来的密钥交换和加密传输做准备。第四步涉及交换密钥,以解密随后将要传输的数据。**直到这第四阶段结束,所有数据仍以明文形式传输,因为此时还不需要加密,因为密钥交换通过数学原理巧妙地避免了明文的需要。**具体来说,客户端和服务器都生成自己的公钥和私钥。客户端的公钥是使用服务器提供的素数和公钥生成的,而服务器的密钥是随机的。此后,双方都可以使用数学运算在本地获得只有他们知道的密钥。随后的传输过程与 Telnet 非常相似。在第五步中,客户端可以选择加密用户名和密码进行身份验证,或使用服务器记录的私钥。一旦身份验证通过,就开始数据传输。值得注意的是,从第五步开始,客户端和服务器之间的所有通信都是加密的,没有密钥几乎无法解密。

数字江湖的暗号

SSH 的强大安全性导致了许多附加功能的发展,如 SSH 转发。SSH 转发解决了 OSI 模型第七层其他协议的安全问题。如幻灯片中所示,考虑一个使用 端口 888 进行通信的应用程序,但该应用程序或协议本身没有加密或解密数据的能力。SSH 转发可以有效地减轻这种风险,确保即使来自这类应用程序的数据也通过加密的 SSH 通道安全传输。

数字江湖的暗号

在这个具体示例中,转发服务使 SSH 能够监控本地主机 端口 888 上的所有数据。一旦需要发送数据包,它就被打包并以 SSH 格式加密,然后路由到预定的目标服务器。当加密的数据包到达目的地并被解密后,服务器端的 SSH 读取它并将数据包转发到相应的本地端口,在这个例子中是 端口 888。在完成这一系列步骤后,不会有明文通过公共互联网传输,从而增强了互联网传输的整体安全性。

数字江湖的暗号

由于其强大的转发能力,SSH 作为 OSI 应用层 的协议,也可以解决 OSI 模型的前三层和物理层相关的问题。例如,在幻灯片中,白色线条代表客户端之间的物理连接。您可以看到,虽然 主机A 和 主机B 没有直接连接,但它们都与 主机C 连接。SSH隧道可以促进 主机A 到 主机B 的数据传输,有效解决缺乏直接连接的问题。这样就绕过了需要额外物理基础设施如路由器、电缆和交换机的需求

数字江湖的暗号

要更好地理解 SSH 隧道,将 主机C 想象为一种路由器是有帮助的。在 主机C 是可信的场景中,来自 主机A 的数据通过 主机C 传输到 主机B。在演示图中,主机A 只与 主机C 开启一个 SSH 通道,并表明其意图将数据转发给 主机B。在传输过程中,主机A 的SSH客户端将其本地 端口688 的数据映射到本地 端口22。这些数据现在被 SSH 加密,发送给 主机C主机C 在验证后,重复这一过程,并将完整的数据包(包括目的端口信息)发送给 主机B主机B 接收到数据后,解密数据,然后将其转发到相应的本地端口,这种情况下是 端口2333

然而,如果在测试转发规则之后确定它们工作正常,并且 主机C 被认为是可信的,那么 主机A主机B 实际上可以忽略 主机C,因为它不会改变数据。在图表中,代表数据流的箭头可能实际上并不存在,但数据仍然得到传输,这类似于在两个位置之间挖掘隧道。这种比喻就是为什么它被称为 SSH隧道。对于用户和高层应用来说,即使在这种情况下也可以忽略 端口22,因为一切看起来就像数据是直接从 主机A 的端口688传输到 主机B端口2333

数字江湖的暗号

接下来,我将解释 SSH 与 FTP 以及 SFTP 之间的关系。FTP 代表文件传输协议,通常使用 端口20端口21。与 SSH 一样,FTP 位于 OSI模型 的第七层,即应用层,也是基于 TCP/IP 的。FTP 的主要功能是在计算机和服务器之间传输文件。用户可以选择使用 SSL 加密传输内容,或选择匿名传输。然而,SFTP 虽然看起来像是在 SSH 上实现的 FTP,但实际上运作方式完全不同。在 FTP 协议中,客户端可以选择两种模式之一:PORTPASV。在 PORT 模式下,客户端向服务器提供一个开放的动态端口,然后服务器向该端口发送文件传输请求。在 PASV 模式下,是服务器开放一个动态端口,客户端发送传输请求。虽然这种设计灵活,但在复杂的网络环境中可能导致问题。例如,如图所示,如果左侧的客户端本地网络有端口控制或防火墙,即使客户端开放了一个动态端口,防火墙可能也不会与之同步,导致外部服务器发送的传输请求被拒绝。PASV 模式不适合一些大规模服务或需要许多客户端同时在线的服务。因此,SFTP 成为这些问题的关键解决方案

数字江湖的暗号

SFTP,作为 SSH 2.0 的一个重要扩展,显著地偏离了大部分 FTP 的传输逻辑。例如,它不是使用 SSL 来加密数据和安全认证信息,而是依赖于一个经过认证且有效的 SSH 连接。这意味着 SFTP 协议本身不提供身份验证;相反,它依赖于底层的 SSH 来提供这项功能。因此,在 SSH 默认禁止匿名传输的情况下,SFTP 也不支持匿名传输。由于加密通过 SSH 处理,所有数据都通过 端口22 进出。这消除了客户端和服务器需要开放随机动态端口的需求。这也意味着,如果一个 SSH 通道成功建立,理论上 SFTP 请求不可能被本地网络内的防火墙阻挡。SFTP 的传输逻辑包括将文件分割成数据包并添加头部,以帮助客户端重新组装它们。这些数据包的加密传输过程本质上与 SSH 中使用的过程相同。

数字江湖的暗号

为了更生动地理解 SSH,我认为最直接的方法是将其与 HTTP/HTTPS 进行比较。如表格的最后两行所示,Telnet 与 SSH 之间的区别类似于 HTTP 与 HTTPS 之间的区别。除了 HTTP/3 外,这四种协议都是基于 TCP/IP 的。Telnet 和 HTTP 在数据安全性方面存在不足,而 SSH 和 HTTPS 与前者相比有些许慢。本质上,从 Telnet 过渡到 SSH 类似于从 HTTP 过渡到 HTTPS。尽管 HTTPS 和 SSH 的加密方法略有不同,但过去 20 年已经证明了这两种协议的成功。[1] [2] [3] [4] [5] [6] [7] [8]


  1. Dusi, Maurizio, et al. “A Preliminary Look at the Privacy of SSH Tunnels.” 2008 Proceedings of 17th International Conference on Computer Communications and Networks, 2008, pp. 1–7. IEEE Xplore, https://doi.org/10.1109/ICCCN.2008.ECP.122.Gasser ↩︎
  2. Oliver, et al. “A Deeper Understanding of SSH: Results from Internet-Wide Scans.” 2014 IEEE Network Operations and Management Symposium (NOMS), 2014, pp. 1–9. IEEE Xplore, https://doi.org/10.1109/NOMS.2014.6838249. ↩︎
  3. Hejtmánek, Lukáš, et al. Choice of Data Transfer Protocols in Remote Storage Applications. 2013. www.muni.cz, https://www.muni.cz/en/research/publications/1130155. ↩︎
  4. Khare, R. “℡NET: The Mother of All (Application) Protocols.” IEEE Internet Computing, vol. 2, no. 3, May 1998, pp. 88–91. IEEE Xplore, https://doi.org/10.1109/4236.683804. ↩︎
  5. Lonvick, Chris M., and Tatu Ylonen. The Secure Shell (SSH) Protocol Architecture. Request for Comments, RFC 4251, Internet Engineering Task Force, Jan. 2006. IETF, https://doi.org/10.17487/RFC4251. ↩︎
  6. Orr, Giles, and Jacob Wyatt. SSH Port Forwarding. 2000. www.usenix.org, https://www.usenix.org/conference/als-2000/ssh-port-forwarding. ↩︎
  7. Postel, J., and J. Reynolds. Telnet Protocol Specification. Request for Comments, RFC 854, Internet Engineering Task Force, May 1983. IETF, https://doi.org/10.17487/RFC0854. ↩︎
  8. Rosasco, Nicholas. How and Why More Secure Technologies Succeed in Legacy Markets: Lessons from the Success of SSH. 2003, https://dash.harvard.edu/handle/1/16781951. ↩︎
]]>
<![CDATA[搞懂摄影]]>https://tiger.work/gao-dong-she-ying/68d66d4d0983ac000162e658Tue, 17 Oct 2023 00:00:00 GMT

“你不可能充满遇见地将生命的点滴串联起来;只有在你回头看的时候,你才发现这些点点滴滴之间的联系。所以,你要坚信,你现在所经历的将在你未来的生命中串联起来。”——乔布斯。

乔布斯的名言似乎能在我的摄影思维路线上被论证,也就是为何在最近,我开始集中思考“为什么摄影?”和“摄影是什么?”这两个问题的时候,这篇文章诞生了。然而我的答案非常简单:和文字一样,摄影无非是一种记录方式。记录的不仅是镜头中、还有镜头之后。

我的摄影之路可以被分为四个阶段,回顾这些阶段,它们启发了我对初心的思考,以及将这些阶段串联起来的冲动。平行时空里2022年的我一定觉得2019年的我是个还在读高中的傻瓜,出去拍摄一直挂着手动档,拍了一内存卡的虚焦却还能在得知了一百张乐色中有一张合焦而暗暗自喜。讽刺的是,2023年的我用着全手动的胶片机器,写文章嘲讽2022年。我没有丝毫自责的脱口道。

19年的我就像细菌走迷宫,手拿相机的场景不是外拍,而是研究。光是切换模式或让照片变得清楚就能耗费我大量的时间。还记得念高中时,住家地下室里的白色桌子和那为了装逼而被竖立着的屏幕(现在我觉得竖屏其实非常不好用)。此刻,知乎成了最好的帮手,但学习技术之余也不免能看到很多大师的作品。我会觉得好看,虽这是一个在意技术的时段。那时的我看来不过是有一座器材的大山挡在面前,但其实审美以及出片的心芽已经被根植。

20年和21年如同凌晨3点钟没有红绿灯的下桥路,这么快技术就已经被我不当回事了。但奇怪的是,我根本不在乎成片。学习压力和在住家积攒的怨气需要一个途径释放,这个重任就交给了外拍。我几乎每周末都会出门,那会儿的热情简直连加拿大的风雨和雪都阻挡不了,是一种享受。看向取景器,在被缩小的景色周围是黑漆无光的边框。眼睛凑近的那一刻,似乎全世界的一切都与我无关了。耳朵能够听到环境,而眼睛却只属于五棱镜中这一片定格的时间。右手食指按动,快门如黑洞将时间吞噬,眼中瞬间全黑。随着咔咔声,反光板重新弹起,像剧场的红色谢幕布。神奇的是,这些是我在这“在意瞬间”的时段里,全部的快乐了。

22年在入手了顶级全画幅单反后,我自认为的限制已经消失了,借口也是。后期修图是我自暴自弃的审美的救星。夸张的时间比例让我认为后期的修改不是伪造现实,而属于写实中艺术创作的部分。和绘画相同,自由的艺术创作因为过于主观而无法批判对与错。在那个时段,我最接近当前我所认为的“摄影的事实”,但22年底才是一切思考的真正开端;年底入手的中画幅无返让我彻底懵了。是的,我最引以为豪的拍摄中一瞬间的感觉,就这么消失了。起初的我将所有愤怒都灌入富士的这台机器,失落、惆怅、想不明白为何。我怪它是个无反,我怪它是个不伦不类的机器,也让我意识到了器材的升级不能总是给我带来快乐。那时的我经常和朋友聊,也经常将这个自我郁闷的时段解释为所谓的瓶颈期。富士的镜头盖死死的关牢着,被我收在柜子里盖灰。我自己也发觉热情正在消减,不愿意出去拍摄。

这一切直到23年开始接触到胶片摄影,才觉得我想明白了一切。在第四阶段,我似乎又回到了第一阶段,毕竟模拟摄影和电子的区别够我重新研究好一阵。对我来说,胶片吸引我的地方不在于反转片那无人不震撼的颜色表达,也不在于专业负片夸张的宽容度,而是它给我带来了前所未有的紧张感,它让我开始忽略后期,将思考集中在拍摄的过程中。也是我第一次开始不仅仅是发布摄影作品,也把精力放在自我欣赏、画册收集、和回看上。我学会了没事也频繁使用幻灯机和观片镜,就为了看看片子高兴高兴。要知道从画册中拿出片子看可比iPhone相册中麻烦多了,而我拍摄电子时只有在反思时才会去重新去回首挖出那份曾经。

胶片就像记忆的冰箱,点滴存入取出。为何不同?在存入的过程中似风轻云淡,但拿出却如风起云涌。“时间是相对的,可以被延长,可以被压缩,但就是无法倒流”。带着观片镜看向底片,如同一口满是开水的蒸锅,但铺面而来,喷发的不是烟雾,是感情、是回忆。心中喷发的是惊喜、是满足。时间无法倒流,但点滴可以保存,只要冰箱功率够大,或经常拿出检查是否变质,那瞬间就能保持新鲜。

回到问题本身,答案是:和文字一样,摄影无非就是一种记录方式,将生活中喜怒哀乐的瞬间——那些点滴——暂存。某天未来从冰箱中拿出,相册中的回忆串联下,千丝万缕涌上心头,百感交集。话说回来,这百感又是什么呢?:包含着自己在摄影路上对每个阶段的思考、不同时期的解读、对相机的感情、对摄影的理解...这又何尝不是点滴?所以才有了孩子好奇的问父母,旧照片上的爸爸为什么和现在不同?答案绝不是割了双眼皮这么简单,重要的是冰冻的瞬间被温暖的感情融化后,向一同观看的人分享,和一同观看的人笑谈!现在来回答,摄影是将我生命中的点滴经历串联的仪式,是对小到野花大到群星的保存与深思,是让瞬间的感动常在,直到我人为父母,直到永远。

搞懂摄影

]]>
<![CDATA[角落气氛]]>https://tiger.work/jiao-luo-qi-fen/68d66bf20983ac000162e642Mon, 08 May 2023 00:00:00 GMT

沉思于开阔,狭小的空间。房间的角落与世界之尽。
所思所想,解放脑海深处。
爵士乐低沉的号声与无边的海,烦恼遁走

—— Provia 100F 所作的诗

角落气氛

角落气氛

角落气氛

]]>
<![CDATA[静谧之处]]>https://tiger.work/jing-mi-zhi-chu/68d66a420983ac000162e629Mon, 13 Mar 2023 00:00:00 GMT

挂着相机,站在洋流之中。与喧闹的学生、狗与遛狗的人邂逅。

晚风拂面,冷飕飕,却压不住这个年龄固有的激奋感受。

空荡的左右,渐暗的天色,与心底无法形容的滋味混合。

好在回首,入眼帘的是那洋流冲不走的常青树,与明月紫霞。

静物与我,似两艘孤舟,止于浓雾世间,迷茫,却伴于无声。

静谧之处

静谧之处

静谧之处

静谧之处

静谧之处

]]>
<![CDATA[轻风吹 摆花树]]>https://tiger.work/qing-feng-chui-bai-hua-shu/68d6673d0983ac000162e610Tue, 21 Feb 2023 00:00:00 GMT

小岛的路不宽,树荫下摆满了车。只可惜不是花瓣飘落的温度,空气中少了点漂浮,笔直的日射打在淡色的房屋涂料上,浓郁的只有绿叶。

橙色安全帽在未完成的房屋框架中忙碌,外翻的皮卡货槽上歇着淡灰色裤腿,挡住了车牌。忙碌着,安静不寂。

早午茶之时,遛狗者徐来。低头抬头之间,横穿了小岛。或许在之后的人生中,可以梦想着拥有其中一只居所,守着日落时帆船亮面漆上的粉色光影。

靠着花盘,卸下行囊,打开胶片后背的舱盖,取出8个定格的瞬间。过片限位齿轮传动的卡嗒声,如秒针,天色渐暗。

轻风吹 摆花树

轻风吹 摆花树

轻风吹 摆花树

轻风吹 摆花树

轻风吹 摆花树

轻风吹 摆花树

]]>
<![CDATA[冬季热诚之海]]>https://tiger.work/dong-ji-re-cheng-zhi-hai/68d665100983ac000162e5f1Tue, 21 Feb 2023 00:00:00 GMT

2022年冬季,第一次使用 Mamiya Universal Press 拍摄 6*9 大小的中画幅彩色胶片。

笨重的机器丝毫没有磨灭对绝景的向往,冒着打不到车的风险,在傍晚跑到了 Laguna 市的一处渺无人烟的海滩。

虽然这卷在3个月后的2023年才被洗出来,但观片台上的反转片给我带来了犹如穿梭回那一刻一般的感觉,身临其境。

巴掌大小的底片分毫毕现,及其自然的颜色过渡和舒适的影调,令我震惊。

回学校时,天边的太阳早已遁走。流连忘返之际,望着街边逐渐亮起的灯火,无比满足。

底片翻拍由 Fujifilm GFX100S 完成:

冬季热诚之海

冬季热诚之海

]]>
<![CDATA[协议的爱恨情仇]]>https://tiger.work/xie-yi-de-ai-hen-qing-chou/68d664ac0983ac000162e5e5Tue, 27 Sep 2022 00:00:00 GMTTCP/IP 是在 IP 协议的通信过程中,使用到的协议族(网络传输协议家族,基础通信架构)的总称。

参考模型

OSI 参考模型

协议的爱恨情仇

开发始于20世纪70年代后期,在20世纪80年代成为国际标准化组织(ISO)的建议。

OSI 的全称是开放系统互连参考模型 (Open System Interconnection Reference Model)。共分为七个抽象层,按连接时的优先级排列分别是:物理层、数据链路层、网络层、传输层、绘画层、表示层、应用层。

TCP/IP 参考模型

最早在高级研究计划局网络项目(ARPANET)中实施。

共分为四个抽象层,按连接时的优先级排列分别是:网络接口层「链路层」、网络层「互联层」(IP)、主机到主机层「传输层」(TCP)、应用层。

TCP/UDP

TCP/UDP 属于传输层的控制协议,接收数据时 优先级最低,仅次于应用层的元数据。

TCP

定义

TCP(Transmission Control Protocol 传输控制协议)是一种面向 连接的、可靠的、基于字节流的 传输层通信协议,由 IETF 的 RFC 793 定义。

连接过程

  1. 第一次握手
    • 客户端向服务器确认意向(SYN或FIN)
  2. 第二次握手
    • 服务器向客户端发送验证(SYN- ACK或ACK)
  3. 第三次握手
    • 建立连接时不执行
    • 结束连接时服务器向客户端发送结束意向,让客户端进入连接终止的等待状态。
    • 结束连接在在建立连接过程的基础上增加了一次握手过程的原因是:如果服务器在第一次握手接到意向后直接关闭了的话,第二次握手发送的验证将无法在第四次握手被接收。所以,通常由客户端执行主动关闭。
  4. 第四(三)次握手
    • 客户端向服务器确认验证

UDP

定义

User Datagram Protocol 简称 UDP。是一种无连接的传输层协议,提供简单不可靠但快速的信息传送服务。即不提供数据包分组,不提供数据包排序。发送后接收方无法得知其是否安全完整的到达。

UDP多用于对高速和实时性需求量大的实例,例如广播、通话、在线会议。免去排序以及分组的步骤可以大幅度提升传输速度,即使代价是丢失一小部分数据(大部分使用UDP的实例中,丢失小部分数据不会造成关键影响)。

综上所述,除了添加首部以外,UDP 在运行过程中将 按照原样 保留应用层发送的用户数据。UDP 是无连接的,即没有认证服务。种种相比于 TCP 减少的功能让UDP的首部保持在了8字节每包,而相比TCP的首部为20字节每包。

UDP 的使用范围很窄,且在部分语言中的实现过程相比于 TCP 来讲要难。

常见的使用UDP的服务:

Python 中 TCP 与 UDP

使用 Socket 库的 Type 更改套接字类型:

  1. SOCK_STREAM >> 推流套接字,TCP 协议
  2. SOCK_DGRAM >> 数据报套接字,UDP 协议
# TCP
import socket
tcp = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
addr = ('192.168.100.1', 25565)
context = "HelloWorld"
tcp.sendto(context.encode(), addr)
tcp.close()

# UDP
import socket
udp = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
addr = ('192.168.100.1', 25565)
context = "HelloWorld"
udp.sendto(context.encode(), addr)
udp.close()

参考 / 引用

“Domain Name System.” 维基百科,自由的百科全书, 27 Sept. 2022. Wikipedia, 链接.
jeanboydev.TCP/IP,TCP,UDP,IP,Socket 之间的关系. 链接. Accessed 22 Sept. 2022.
“OSI Model.” 维基百科,自由的百科全书, 21 Sept. 2022. Wikipedia, 链接.
“TCP/IP协议族.” 维基百科,自由的百科全书, 9 June 2022. Wikipedia, 链接
张超帅. UDP的结构和传输原理. 链接. Accessed 27 Sept. 2022.
涤生大数据. Python中socket与UDP使用与通信详解. 链接. Accessed 27 Sept. 2022.

]]>
<![CDATA[某篇学习笔记]]>https://tiger.work/mou-pian-xue-xi-bi-ji/68d664680983ac000162e5d9Thu, 22 Sep 2022 00:00:00 GMTData Type某篇学习笔记

string: immutable, slicing

list: mutable, slicing, nested, multidimentional, ordered

dict: mutable, no ordered, no dupilicated key

tuples: immutable, sequence, better performance than list

set: no ordered, immutable

int: not iterable

Class

inheritance: keep all methods and functions

super(), make child class inherit all the methods and properties from its parent

class Student(Person):
  def __init__(self, fname, lname):
    super().__init__(fname, lname)

staticmethod: can be called without an object for that class, passed no extra object

class Calculator:
    def addNumbers(x, y):
        return x + y

# create addNumbers static method
Calculator.addNumbers = staticmethod(Calculator.addNumbers)

print('Product:', Calculator.addNumbers(15, 110))

class attributes: shared by all instances at the class level

class Person:
    num_instances = 0
    def __init__(self):
        Person.num_instances = Person.num_instances + 1

class method: process class data instead of instance data, passed a class object

class Person:
    num_instances = 0
    def __init__(self):
        Person.num_instances = Person.num_instances + 1
    @classmethod
    def print_num_instances(cls):
        print(cls.num_instances)

Instance methods: passed a self instance object by default

Iterator

An iterator must have two methods: iter and next

When it reaches the end, will give an error

You can convert iterators to lists or tuples

why? More efficient for handling larger datasets.

L = [1, 2, 3]
it = iter(L)
it.__next__()
next(it)

Generator

create your own iterators with generators in Python.

def fib2(limit):
    a, b = 0, 1
    while a < limit:
        yield a # call fib2() output a, next() output next a
        a = b
        b = a + b
  1. Using generators save memory and time because they only provide you and compute what you need for now.
  2. Using generators can also save the current state for you if you need it for future computations.

Recursion

a function within which it calls itself.

def mysum(l):
    print(l)
    if not l:
        return 0
    else:
        return l[0] + mysum(l[1:])

Recursion error can also happen due to stack overflow

Regular Expression

specify the rules for the set of possible strings that you want to match

[a-c]: length 1, if a-c
[^a]: length 1, if not a
ap*: length unlimited, if a or a with any num of p
ap+: length >= 2, if ap or a with any num of p, num!=0

re.findall() returns a list containing all matches

re.search() returns the first match as a Match object
.start().end().span().string.group(), return none if no match

re.sub(pattern, repl, string, count=0, flags=0)

File

os.walk: Iterate a directory

import os
rootdir = './' # or you can change to any of your directory that you want to iterate over

for subdir, dirs, files in os.walk(rootdir):
    for file in files:
        print(subdir)
        print(os.path.join(subdir, file))
        # ./
				# ./Lec8-HandOut.ipynb
import os
rootdir = './'

for subdir, dirs, files in os.walk(rootdir):
    for file in files:
        if file.endswith('.txt'):
            print(os.path.join(subdir, file))

Argparse

args = sys.argv[1:]
parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('--mode', type=str, required=True)
args = parser.parse_args()

Stack

last in first out, first in last out

Linked List Format - 每一个node 的next是前面一个node

push等于加一个新node并把上一个node放在next,之后把当前node变成上一个node

pop等于把上一个node变成当前node的next,因为没有了reference,自动就删除了

# A linked list implementation
class Node:
    def __init__(self, value):
        self.value = value
        self.next = None
class Stack:
    def __init__(self):
        self.head = Node("Head")
        self.size = 0
    
    # Override the pring function to print more user-friendly stack
    def __str__(self):
        cur = self.head.next
        out = ""
        while cur:
            out += str(cur.value) + "->"
            cur = cur.next
        return out[:-3]
 
    # Get the current size of the stack
    def getSize(self):
        return self.size
 
    # Check if the stack is empty
    def isEmpty(self):
        return self.size == 0
 
    # Get the top item of the stack
    def peek(self):
 
        # Sanitary check to see if we
        # are peeking an empty stack.
        if self.isEmpty():
            raise Exception("Peeking from an empty stack")
        return self.head.next.value
 
    # Push a value into the stack.
    def push(self, value):
        node = Node(value)
        node.next = self.head.next
        self.head.next = node
        self.size += 1
 
    # Remove a value from the stack and return.
    def pop(self):
        if self.isEmpty():
            raise Exception("Popping from an empty stack")
        remove = self.head.next
        self.head.next = self.head.next.next
        self.size -= 1
        return remove.value
 
 
# Driver Code
if __name__ == "__main__":
    stack = Stack()
    for i in range(1, 11):
        stack.push(i)
    print(f"Stack: {stack}")
 
    for _ in range(1, 6):
        remove = stack.pop()
        print(f"Pop: {remove}")
    print(f"Stack: {stack}")

Queue

first in first out, last in last out

from collections import deque
q = deque()
q.append('a')
q.append('b')
q.append('c')
print(q) # deque(['a', 'b', 'c'])
q.appendleft('e')
print(q) # deque(['e', 'a', 'b', 'c'])
print(q.popleft()) # e
print(q) # deque(['a', 'b', 'c'])
print(q.pop()) # c
print(q) # deque(['a', 'b'])
q.count('a') # 1

linked list - head为表头,next代表下一个node

insert - 创建node,如果没有head则定为head,如果有head则loop到最后一位并设到next

delete - 创建node,loop到最后并给prev和cur设置reference,删除prev的next

列表中的数必须按照从小到大的顺序排列

查询过程 - 每次取两个index值,并取一个mid,如果mid不是对象,则根据对象大小改变first和last

原理,如果对象比查询要小,则仅查询mid之前,如果对象比查询要大,则仅查询mid之后

def binarySearch(alist, item):
    first = 0
    last = len(alist)-1
    found = False
    while first<=last and not found:
        midpoint = (first + last)//2
        print(first, last)
        print(midpoint)
        if alist[midpoint] == item:
            found = True
            return midpoint
        else:
            if item < alist[midpoint]:
                last = midpoint-1
            else:
                first = midpoint+1
    return midpoint
    
binarySearch([1,2,2,3,4,5],2)

Bubble sort

每一次loop往前摆一个位置,只在两个数中调换

def bubbleSort(array):
    
    for i in range(len(array)):

        for j in range(1, len(array) - i - 1):
#             print(i, j)
            if array[j] > array[j + 1]:
                temp = array[j]
                array[j] = array[j+1]
                array[j+1] = temp
#                 print(array)
    return array
    
data = [-1, 4, 0, 5, -3]
bubbleSort(data)

Insertion Sort

每次从前面的最近比较到最远,如果前一位更大,ref后把当前数换到前一位,继续比,如果前一位更小或者没有,把当前数换成ref

每一次loop把当前数摆到正确的位置

def insertion_sort(lst):
    for i in range(1, len(lst)):
        val = lst[i]
        j = i - 1
        while j >= 0 and val < lst[j]:
            lst[j+1] = lst[j]
            j = j - 1
        lst[j+1] = val
    return lst
insertion_sort([0,-1,2,1,-3])

Selection sort

从头开始loop,当前值是index,loop一遍后面的值找到最小的,和当前值替换。所以当到第二位时,前面的值一定等于或者小于前面所有的值。

def selection_sort(lst):
    size = len(lst)
    for cur in range(size):
        min_pos = cur
#         min_val = lst[cur]
        for i in range(cur + 1, size):
            if lst[i] < lst[cur]:
                min_pos = i
        tmp = lst[min_pos]
        lst[min_pos] = lst[cur]
        lst[cur] = tmp
    return lst
]]>
<![CDATA[寻找史莱姆的家]]>https://tiger.work/xun-zhao-shi-lai-mu-de-jia/68d663e10983ac000162e5cdMon, 18 Apr 2022 00:00:00 GMT分析寻找史莱姆的家

Minecraft 主要用 Java 编写,史莱姆区块的生成是采用了伪随机算法。伪随机意为在同样的种子下,生成的结果是相同的。所以当有了种子之后,可以推算出几乎所有自然生成结构的位置。甚至,如果你有大量有效的自然生成结构位置,可以反推出种子。

// https://minecraft.fandom.com/wiki/Slime
import java.util.Random; 
public class checkSlimechunk{ 
    public static void main(String args[]) 
    { 
        // the seed from /seed as a 64bit long literal
        long seed = 12345L;
        int xPosition = 123;
        int zPosition = 456;
        Random rnd = new Random(
            // 第一部分
            seed +
            // 第二部分
            (int) (xPosition * xPosition * 0x4c1906) +
            // 第三部分
            (int) (xPosition * 0x5ac0db) +
            // 第四部分
            (int) (zPosition * zPosition) * 0x4307a7L +
            //第五部分
            (int) (zPosition * 0x5f24f)
            ^ 0x3ad8025fL
        );
        System.out.println(rnd.nextInt(10) == 0);
    } 
}

这是wiki中扒出的游戏算法,这段代码可以理解为:伪随机种子由5部分构成,五部分相加后按位异或一个 long 值得出进入随机前的最终种子。五部分中,一共需要3个输入值,分别是 long 类种子、int 类区块x坐标、int 类区块z坐标。

  • 五部分的第一部分是游戏中 /seed 获得的种子,类是 long 。按照 intlong 之间加减的转换顺序和随机种子算式的优先级,所有之后的五部分都会最终转换成 long
  • 第二部分为x坐标平方再乘一个十六进制数 0x4c1906,换算成十进制就是 4987142,在 Python 中不需要换成十进制计算,直接相乘。
  • 第三部分同理。
  • 第四部分注意,z平方是在 int 类中完成计算,在乘括号外的十进制时需要转化成 long
  • 最后同样也是一个 int 类计算,要注意最后的按位异或的优先级在java中是比加减乘除要低的,所以应该是所有部分加起来后再运算。

在RNG对象赋值好后,最后是用 .nextInt() 输出一个0-9的数字,如果数字是 0 ,则该区块是史莱姆区块。

实现

框架构建
class slime(object):
    def __init__(self):
        self.seed = None
        self.x = None
        self.z = None

if __name__ == "__main__":
    obj = slime()
Random Number Generator 种子计算
def javaInt(self, val):
    limit = 2147483647
    if not (-1*limit)-1 <= val <= limit:
        val = (val+(limit+1))%(2*(limit+1))-limit-1
    return val
def javaLong(self, val):
    limit = 9223372036854775807
    if not (-1*limit)-1 <= val <= limit:
        val = (val+(limit+1))%(2*(limit+1))-limit-1
    return val

Python 3中int类大小是无限的,而java中int和long都有上限。越过限制后,会从另一端继续计算,例如int的上限是 2,147,483,647,这个数+1等于**-2,147,483,648**,+2等于**-2,147,483,647 **以此类推。根据上方算法分析,坐标乘以的十六进制数算是比较大的,所以有很大可能性会过界,所以必须要有一种溢出算法能让python的不过界数模你出 Java的过界效果。

def rngSeed(self):
    a = self.javaLong(self.javaInt((self.x**2)*0x4c1906))
    b = self.javaLong(self.javaInt(self.x*0x5ac0db))
    c = self.javaLong((self.javaInt(self.z**2)*0x4307a7))
    d = self.javaLong(self.javaInt(self.z*0x5f24f))
    return self.javaLong(((self.seed+a+b+c+d)^0x3ad8025f))

设置好溢出后套入算法即可,需要注意 Java的long类会在对象后加一个L和int类区分开来,在python中要把L去掉,比如 0x3ad8025fL 就变成 0x3ad8025f。

模拟 java.util.Random 算法
# https://github.com/MostAwesomeDude/java-random
class javaRandom(object):
    def __init__(self):
        self.seed = None
    def setSeed(self, seed):
        self.seed = (seed^0x5deece66d)&((1<<48)-1)
    def next(self, bits):
        if bits < 1:
            bits = 1
        elif bits > 32:
            bits = 32
        self.seed = (self.seed*0x5deece66d+0xb)&((1<<48)-1)
        retval = self.seed>>(48-bits)
        if retval & (1<<31):
            retval -= (1<<32)
        return retval
    def nextInt(self, n):
        if n > 1:
            if not (n&(n-1)):
                return (n*self.next(31))>>31
            bits = self.next(31)
            val = bits%n
            while (bits-val+n-1) < 0:
                bits = self.next(31)
                val = bits%n
            return val

参考 MostAwesomeDude 的模拟 Java 伪随机算法,去除了不需要的一些next方法,只留下了分析中需要的 .nextInt。这个类是参照官方文档对 Random() 定义写出的,并且做了Python的适配,可以直接使用。

关于 Java Random() 的介绍,参考 java.util - Class Random
结果转换
def check(self, seed, x, z):
    # 输入
    self.seed = seed
    self.x = x
    self.z = z
    # 最终种子
    rng = self.rngSeed()
    # 初始化
    obj = javaRandom()
    # 设置最终种子
    obj.setSeed(rng)
    # 得到结果
    rtn = obj.nextInt(10)
    # 判断结果
    if rtn == 0:
        return True
    else:
        return False

总结

源码
class slime(object):
    def __init__(self):
        self.seed = None
        self.x = None
        self.z = None
    def javaInt(self, val):
        limit = 2147483647
        if not (-1*limit)-1 <= val <= limit:
            val = (val+(limit+1))%(2*(limit+1))-limit-1
        return val
    def javaLong(self, val):
        limit = 9223372036854775807
        if not (-1*limit)-1 <= val <= limit:
            val = (val+(limit+1))%(2*(limit+1))-limit-1
        return val
    def rngSeed(self):
        a = self.javaLong(self.javaInt((self.x**2)*0x4c1906))
        b = self.javaLong(self.javaInt(self.x*0x5ac0db))
        c = self.javaLong((self.javaInt(self.z**2)*0x4307a7))
        d = self.javaLong(self.javaInt(self.z*0x5f24f))
        return self.javaLong(((self.seed+a+b+c+d)^0x3ad8025f))
    def check(self, seed, x, z):
        self.seed = seed
        self.x = x
        self.z = z
        rng = self.rngSeed()
        obj = javaRandom()
        obj.setSeed(rng)
        rtn = obj.nextInt(10)
        if rtn == 0:
            return True
        else:
            return False

class javaRandom(object):
    def __init__(self):
        self.seed = None
    def setSeed(self, seed):
        self.seed = (seed^0x5deece66d)&((1<<48)-1)
    def next(self, bits):
        if bits < 1:
            bits = 1
        elif bits > 32:
            bits = 32
        self.seed = (self.seed*0x5deece66d+0xb)&((1<<48)-1)
        retval = self.seed>>(48-bits)
        if retval & (1<<31):
            retval -= (1<<32)
        return retval
    def nextInt(self, n):
        if n > 1:
            if not (n&(n-1)):
                return (n*self.next(31))>>31
            bits = self.next(31)
            val = bits%n
            while (bits-val+n-1) < 0:
                bits = self.next(31)
                val = bits%n
            return val

if __name__ == "__main__":
    obj = slime()
    exe = obj.check(seed=1664026956323281049, x=-9, z=-17)
    print(exe)
    
返回
$ python3 demo.py
True
]]>
<![CDATA[让代码有点“颜值”]]>https://tiger.work/rang-dai-ma-you-dian-yan-zhi/68d663810983ac000162e5c1Thu, 07 Apr 2022 00:00:00 GMTTkinter 初始化
主对象的创建与事件循环的开始
import tkinter as tk

root = tk.Tk()
root.mainloop()
让代码有点“颜值”

将 root 作为 tkinter 基层窗口的实例化 object,这步操作会初始化窗口。如果程序在分配窗口之前使用了其他 tkinter 的方法,则会报 RuntimeError。初始化到 .mainloop 方法之间的部分为部件设计,所以用 .mainloop 来给实例化object的设计部分结尾,而 .mainloop 则代表开始进入事件循环,监控窗口事件。

关于如何理解 .mainloop,参考 Tkinter 中的 mainloop 应该如何理解.
窗口全局设置
root.title("Tiger's Notes")
# 窗口标题
root.geometry("1920x1080")
# 窗口初始大小
root.geometry("500x300+200+50")
root.geometry("500x300+-90+-10")
# 移动窗口在屏幕上的位置

窗口名字可以是任意 string,初始大小中的两个数字中间的乘号是小写的x,同样也需要是 string。如果在.mainloop 之前不做全局设置,则窗口标题默认为 tk,窗口大小默认为基层容器的长和宽。

如果将 .geometry 的语法写成 "axb+c+d" ,A和B正常读取,C和D则读为窗口在屏幕上的位置。 root.geometry("500x300+200+50") 意思为,将大小设置为500*300并将其放在屏幕坐标 (200, -50) 处。屏幕坐标的原点是最左上角。加号后方的数字也可以是负数,负数代表反向移动。

root.geometry("500x300+200+50")
print(type(root.geometry(None)))
# <class 'str'>
print(root.geometry(None))
# 500x300+200+50

如果将 .geometry 的 arg 换成 None,则返回一个当前窗口大小与位置信息的 string。返回格式与设定时的输入格式一样。

全局设置的动态更新
import tkinter as tk
import random

root = tk.Tk()

def changeTtlandSize():
    xRange = random.randint(500, 1000)
    yRange = random.randint(300, 600)
    displaySize = str(xRange) + "x" + str(yRange)
    root.title(displaySize)
    root.geometry(displaySize)
    
root.title("Click that button!")
buttonA = tk.Button(root, text="Click to change size", command=changeTtlandSize)
buttonA.pack()
root.mainloop()

全局设置貌似不能用 .StringVar 动态更新,但是可以在 .mainloop 循环中使用 command= 重新设置。上面的代码创建了一个窗口XY值分别在 500-1000 以及 300-600 之间取随机数后,使用该数值刷新窗口大小并且刷新窗口标题。

.geometry 方法是默认忽略 .resizable 的,意思说就算已经禁止调整宽高,还可以使用 .geometry 调整窗口。在使用了 .resizable 后,只有用户直接在视窗上的操作受到影响。

关于 .resizable 的具体用法,参考 resizable() method in Tkinter | Python

Tkinter 添加窗口部件

配置部件
tkObject.pack()
# 部件放置
tkObject.pack_forget()
# 部件隐藏体积;设为不可见
tkObject.destroy()
# 部件永久删除

.pack() 的安置算法是完全按照顺序,默认占有整行,但可以使用 side=anchor= 等属性来实现同行或者其他形式的安置。任何部件在父容器中出现的位置不是由设定函数的位置所决定顺序,而是由安置方法的顺序决定。

tkObject.grid(row=3, column=2)
# 第四行第三列
tkObject.grid(row=0, sticky=W)
# 第一行靠左
tkObject.grid_forget()
# 部件隐藏体积;设为不可见

.grid() 的安置算法是在父容器中模拟出一个表格,使用坐标位置放置。此方法在横向放置上较为容易。可以使用 sticky= 属性在长宽不规则的单元中限制部件的为位置。

关于 .grid() 的各种属性,参考 Python Tkinter Grid 布局管理器详解
Label 标签
root = tk.Tk()
var = tk.StringVar()
var.set("Tiger's Notes")
textInfoA = tk.Label(root, text="Tiger's Notes", font=('Arial', 60))
textInfoA.pack()
textInfoB = tk.Label(root, textvariable=var)
textInfoB.pack()
imageTk = tk.PhotoImage(file="/Users/tiger/Downloads/JiaRanDiana.png")
imageInfo = tk.Label(root, image=imageTk)
imageInfo.pack()
root.mainloop()

Label 标签中可以添加文字、动态文字、图片,具体用法如上。使用 font= 参数设置字体与大小,使用 textvariable= 参数设置动态文字的对象,使用 image= 参数设定 PhotoImage 对象。这里注意,image= 后面不能直接跟图像路径,需要使用 tk.PhotoImage(file=) 将本地图片先转化为 Tkinter 自己的图片格式。另外,目前受直接支持的图片格式有 GIF, PGM, PPM, PNG。如果对象是其他格式的话可以使用 PIL 的 Image 工具转换。

关于使用 PIL 转换图片格式,参考 基于 Python PIL 实现简单图片格式转化器
import tkinter as tk
from PIL import Image, ImageTk
import io
import requests

root = tk.Tk()
# 对象照片链接
url = "https://img.gejiba.com/images/43b77154489b7b0548d7386381f6eb03.png"
imageTKForm = ImageTk.PhotoImage(Image.open(io.BytesIO(requests.get(url).content)))
imageInfo = tk.Label(root, image=imageTKForm)
imageInfo.pack()
root.mainloop()

Label 还可以配合 ImageTk、io、requests 实现网页图片爬取并在窗口中显示,上方代码的方案全程没有文件储存,是完全基于在线处理。首先通过 requests.get().content 获取编码形式的图片,io.BytesIO 方法将编码写入 RAM,最后使用 PIL 的 Image 解码后用 ImageTk 转化为 Tkinter 格式。

关于 io 模块的具体用法,参考 StringIO 和 BytesIO
Button 按钮 / Entry 输入域
root = tk.Tk()
def getAndPrint():
    print(textInputBox.get())
textInputBox = tk.Entry(root, show=None)
textInputBox.pack()
getButton = tk.Button(root, text="Get and Print!", command=getAndPrint)
getButton.pack()
root.mainloop()

Button 为实体按钮,上面可以部署文字,也可以部署触发指令。这个指令可以是自带环境中的函数例如 quit(),也可以是自定义函数,需要注意 command= 后面的函数名不用带正反括弧也不用加引号。

Entry 为文本输入框,用户可以在界面中输入文字。当使用 .get() 方法时,获取当前存在于 Entry 中的信息,返回 String。文本输入框也可以有 text= 或者 textvariable= 属性,设置文字用于提醒用户在文本框内需要输入什么。

Label 的一个缺点是无法复制,这个问题的其中一个解决方法是用 Entry 并把输入框设定为只读模式,具体用法为 entry['state'] = 'readonly' 。这样文本框内显示的内容就可以被选中。show=None 属性限制文本框显示明文,使用 entry['show'] = '*' 即可让特定符号 "*" 代替显示内容,常用于密码输入。

关于 Tkinter Button 的详细使用方法,参考 tkinter-button 详解
Variable 动态
# 使用 StringVar 方法创建动态 String
root = tk.Tk()
var = tk.StringVar()
var.set("Type something!")
dynamicLabel = tk.Label(root, textvariable=var)
dynamicLabel.pack()
def refreshText():
    var.set(usrInput.get())
usrInput = tk.Entry(root, show=None)
usrInput.pack()
refreshButton = tk.Button(root, text="Refresh", command=refreshText)
refreshButton.pack()
root.mainloop()

Variable 的存在是为了解决 Python 不支持变量回溯的问题。目前 Tkinter 提供了总共 4 种 Variable,分别是 [BooleanVar(), DoubleVar, IntVar, StringVar] ,它们主要是 Type 上的区别。

主要方法为 .set().get(),用于拉取数据和设置数据。需要注意,拉取动态函数的时候会得到动态函数指定的 Type。举个例子,如果在开发环境中有一个别的模组中不是 String 的文字段,虽然它和 String 有几乎一样的 Library,但是拉取的时候还是会变成 String。

关于 Variable 函数的具体实现方法,参考 Python基础知识-GUI编程-TK-StringVar
Frame 容器
root = tk.Tk()
var = tk.StringVar()
var.set("Hide")
mainMenu = tk.Frame(root, height=100, width=100)
mainMenu.pack()
textInfo = tk.Label(mainMenu, text="Tiger's Notes")
textInfo.pack()
def hideOrDisplay():
    if var.get() == "Hide":
        var.set("Display")
        textInfo.pack_forget()
    else:
        var.set("Hide")
        textInfo.pack()
buttonA = tk.Button(root, text="Hide/Display", command=hideOrDisplay)
buttonA.pack()
root.mainloop()

Tkinter 中的 Frame 用于组织小部件,简单来说就是一个像 root 基层容器的子集容器。Frame 具有不可见实体,所以在 .pack() 后位置是固定的。如果选择按顺序放置零部件,则可以使用 Frame 充当占位符。这种用法比较适合在小部件的变量无法在运行中动态更新的情况下,比如说特殊的 type 例如图片的更新。

正确的用法为 frameA = tk.Frame(parent, options)。合理运用 options 也可以达到优化界面的效果,比如设置背景、长宽等。当需要将小部件放入 Frame 容器中,小部件的 parent 需要设置为容器 Object。

# Frame 使用 .winfo_children().destory()
root = tk.Tk()
dynamicFrame = tk.Frame(root)
dynamicFrame.pack()
dynamicLabel = tk.Label(dynamicFrame, text="Type something!")
dynamicLabel.pack()
def refreshText():
    for dynamicItem in dynamicFrame.winfo_children():
        dynamicItem.destroy()
    dynamicLabel = tk.Label(dynamicFrame, text=usrInput.get())
    dynamicLabel.pack()
usrInput = tk.Entry(root, show=None)
usrInput.pack()
refreshButton = tk.Button(root, text="Refresh", command=refreshText)
refreshButton.pack()
root.mainloop()

假设 Label 没有 textvariable= 这个属性可用,可以使用上面的方法在让某个小部件不改变全局位置的情况下进行自身属性完全刷新。.winfo_children() 方法会返回 [<tkinter.Label object .!frame.!label>]。这是一个包含了 Frame 中所有 Object 的列表。

主要阅读

辅助参考

]]>
<![CDATA[网页信息狙击术]]>https://tiger.work/wang-ye-xin-xi-ju-ji-zhu/68d663200983ac000162e5b5Mon, 04 Apr 2022 00:00:00 GMTSelenium v4 与 Webdriver 的安装
$ pip3 install -U selenium
$ pip3 install webdriver_manager
网页信息狙击术

Selenium 4.0.0 即将移除 webdriver 中的 executable_path= 标签。如果手动安装 webdriver 后用 executable_path= 指定路径,则会跳出警告,影响输出。

Webdriver 初始化与连接网页

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
driver.get("https://www.google.com")

如果需要加上浏览器选项,则在 Service() 中加上 options=options

常用 Webdriver 选项

options = webdriver.ChromeOptions()
# 初始化 options 作为设置对象
options.add_argument('--no-sandbox')
# 解决DevToolsActivePort文件不存在的报错
options.add_argument('--disable-gpu')
# 关闭 GPU(可规避部分Bug)
options.add_argument('window-size=1920x1080')
# 设置浏览器初始窗口大小
options.add_argument('--hide-scrollbars')
# 隐藏滚动条
options.add_argument('--headless')
# 无头模式,不提供可视化页面,Dos系统必须加不然报错
options.add_experimental_option("excludeSwitches", ["ignore-certificate-errors", "enable-automation"])
# 开发者模式启动,规避检测
option.add_argument("--disable-javascript")
# 禁用JavaScript

如果要达成完全在后台操作的效果,有图形界面的电脑也可以使用 .add_argument('--headless') 切换为无头模式。

更多 Chromium 命令行开关,参考 List of Chromium Command Line Switches

Selenium 使用不同方法定位元素

from selenium.webdriver.common.by import By

driver.find_element(By.[arg]), argValue)
driver.find_elements(By.[arg]), argValue)
.find_element()
from selenium.webdriver.common.by import By

webElement = driver.find_element(arg, argValue)

arg 可以替换成任何By类可用的属性,argValue 可替换为属性的具体值,type 需要是 str。By 类可用的属性在下方表格:

arg argValue
By.ID id="id"
By.XPATH "XPath"
By.LINK_TEXT href="link text"
By.PARTIAL_LINK_TEXT href="tigerding.com/partial_link_text"
By.NAME name="name"
By.TAG_NAME <tagname></tagname>
By.CLASS_NAME class="class"
By.CSS_SELECTOR <p class="content"></p> "p .content"
关于 By.CSS_SELECTOR 的进阶使用,参考 Selenium Tips: CSS Selectors
.find_elements()
from selenium.webdriver.common.by import By

driver.get(baseURL + seedInput + locationX + locationY + otherAttri)
htmlElements = driver.find_elements(By.TAG_NAME,'script')
for eachElement in htmlElements:
    print(eachElement.get_attribute('innerHTML'))

这个方法和 .find_element 基本一样,只是返回的值是一个 list,list 中包含所有符合 arg 的 WebElement 类。

.get_attribute('innerHTML') 用于打印单个元素的 html 源码,如果这里不加上这个方法光用 print(eachElement) 的话,会打印出 <class 'selenium.webdriver.remote.webelement.WebElement'> 而不是 html string。

save_screenshot 与 screenshot 的区分

driver.get("https://google.com/")
googleLogo = driver.find_element(By.XPATH, '/html/body/div[1]/div[2]/div/img')
fileDir = "/Users/tiger/Downloads/google.png"
googleLogo.screenshot(fileDir[:])
print("image saved")

>>> python3 demo.py
image saved

如上方代码块所示,当需要截图的对象是已经被定位到的某个元素时,无法使用 .save_screenshot() 。因为在 WebElement 类中这个方法不是这样写的,换成了 .screenshot()。后方的 arg 完全一样,都是带有具体文件目录,包含文件名和格式。

当截图对象是已经是被定位到的某个元素时,使用 .screenshot() 即可给对应元素截图,大小为元素的原始大小,默认保存格式为png。这里要注意,针对指定元素的截图不能使用 .save_screenshot() 否则返回 AttributeError

driver.get("https://google.com/")
driver.save_screenshot("./google.png")
print("image saved")

>>> python3 demo.py
image saved

.save_screenshot() 是用在整个浏览器截图上,截图大小为浏览器窗口大小。虽然可以通过 iamge.crop((w,t,h,b)) 对浏览器窗口进行剪裁达到对元素截图的同样目的,但是需要注意必须在 webdriver.ChromeOptions() 设置好窗口大小,不然会默认为电脑屏幕的大小,裁剪不好容易跑偏。

关于 PIL 的 Image 图形处理外部库,参考 Python图像库PIL的类Image及其方法介绍

Webdriver 设置窗口大小为网页大小

browserWidth = driver.execute_script("return document.documentElement.scrollWidth")
browserHeight = driver.execute_script("return document.documentElement.scrollHeight")
driver.set_window_size(browserWidth, browserHeight)

document.documentElement 这个这个Web API 指向一般目标网站的 rootElement, 也就是最底层的元素。使用 .scrollWidth.scrollHeight 获得元素长宽,一般一整个网页的长宽就是底层元素的长宽。

.set_window_size() 是在初始化浏览器后改变窗口大小的方法。在初始化浏览器前可以使用 webdriver.ChromeOptions().add_argument(window-size=width[x]height)

阅读文章

辅助资料

]]>
<![CDATA[烹饪一道数据大餐]]>https://tiger.work/peng-ren-yi-dao-shu-ju-da-can/68d662a70983ac000162e5a9Fri, 01 Apr 2022 00:00:00 GMTRequests

.get 语法

import requests

req = requests.get(url = target)
# 发送请求,target必须是type为str的完成网页链接
htmlOutput = req.text
print(type(htmlOutput)) # string
# 格式整理

Beautiful Soup

烹饪一道数据大餐

提取html长串中感兴趣的内容,可使用正则表达式。

.find_all 语法

获取单个标签
from bs4 import BeautifulSoup

bfObject = BeautifulSoup(htmlString)
texts = bf.find_all('div', class_ = 'Tiger')

获得htmlString中的标签 <div id="content", class="Tiger"> 并assign给texts。texts的type为 bs4.element.ResultSet 的 list。如果htmlString中含有多个相同标签,则按照顺序排列在list中。

texts = bf.find_all('div', class_ = 'Tiger') 中使用 class_ = 而非 class = 是为了防止冲突。

第四行也可替换为 find_all(‘div’, id = ‘content’, class_ = ‘Tiger’)。在html中,id具有唯一性,一个网页中仅能用一次。class可以任意使用。id对应CSS中的 "#" 而class对应 "."。重要、特别的内容盒子使用id,局部使用class。

关于更多id与class,参考 在html和css中使用id和class的区别
GuessedAtParserWarning
/Users/tiger/Desktop/demo.py:6: GuessedAtParserWarning: No parser was explicitly specified, so I'm using the best available HTML parser for this system ("html.parser"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently.

Parser 意为解析器,是一个结构化标记处理工具,主要用途是分析HTML文件。使用 bf = BeautifulSoup(htmlString, features="html.parser") 即可移除警告输出。

关于 html.parser,参考 Simple HTML and XHTML parser
获取多个标签
<!-- html String -->
<a href="/a/bcde/1.html">某超链</a>
<a href="/a/bcde/2.html">某超链</a>
<a href="/a/bcde/3.html">某超链</a>
<a href="/a/bcde/4.html">某超链</a>
<a href="/a/bcde/5.html">某超链</a>
bf = BeautifulSoup(htmlString)
htmlTags = bf.find_all('a')
for eachTagA in htmlTags:
	print(eachTagA.string, eachTagA.get('href'))
  # 某超链 /a/bcde/1.html

上面代码中,htmlTags的值为[<a href="/a/bcde/1.html">某超链</a>, ...],list中的每一个元素的type为 bs4.element.Tag,和str有些许区别,某些str library不能直接用。

当对象的type为 bs4.element.ResultSet 时(htmlTags的type),.string 用于提取出HTML标签中的内容,.get("attribute") 用于提取出标签中的某属性。

参考文章

笔记

  • robots.txt协议中规定了网站中哪些数据可以被爬取哪些数据不能被爬取
]]>