关于 Nekonull | 卢之睿

学生、程序员、设计师,腾讯和商汤实习。

RSS 地址: https://nekonull.me/index.xml

请复制 RSS 到你的阅读器,或快速订阅到 :

Nekonull | 卢之睿 RSS 预览

工具小站

2024-10-07 14:03:00

参考 tools.simonwillison.net,决定把所有单页纯前端应用全部放到到一个 repo 里,并且分配一个子域名;这样不用每个小工具都申请独立 repo + 配置 Github Actions 了。

虽然目前还是没什么东西,但是还是欢迎来玩:pages.nekonull.me

基于 WebRecorder 和 MitmProxy 的图片手动抓取探索

2024-10-07 13:58:00

最近收到任务,需要从某个站点下载一系列图片。首先当然是 F12 看下网络请求。这是一个无限滚动的瀑布流,图片地址本身是随机的(看起来文件名像是 uuid),所以没法直接遍历图片地址;此外每次滚动到底,都会触发一个获取下一页图片地址的请求,参数传的还是游标而不是页数,这又断绝了直接构造分页请求(例如 page=1)获取所有图片地址,再逐个拉取的念头。好在总图片数是有限的,大概在 2k 左右,每次下拉能拉回 50 条,所以手动抓取也不是不能接受。以下是两种手动抓取的思路。注:这里的手动抓取,指的是在浏览器前端通过模拟人类行为,无侵入且不对网站发起额外请求的抓取方式。

前置准备:AHK 翻页器

一个简单的小工具,每隔一段时间自动按 page down 翻页(建议提前 zoom out 调小页面比例,这样滚动起来效率更高)

AHK 翻页器代码
#Requires AutoHotkey v2.0

; 创建一个 GUI
MyGui := Gui()
MyGui.Add("Text", "x10 y10", "Auto Page Down:")
toggle := MyGui.Add("Checkbox", "x10 y30 vToggleState", "Enable")
MyGui.OnEvent("Close", (*) => ExitApp())
MyGui.Show()

; 设置全局变量以跟踪定时器状态
global keyHeld := false

; 定时器:每0.1秒检查一次开关状态
SetTimer(CheckToggleState, 100)

; 主循环
CheckToggleState()
{
    global keyHeld  ; 声明 keyHeld 为全局变量

    if (toggle.Value) {
        if (!keyHeld) {
            ; 如果勾选了启用选项,并且哔哩哔哩窗口处于活动状态
            RandomDelayAndSendPageDown()
            keyHeld := true
        }
    } else {
        if (keyHeld) {
            ; 如果没有勾选启用选项,停止按键操作
            keyHeld := false
        }
    }
}

; 生成随机延迟并发送 Page Down 键
RandomDelayAndSendPageDown()
{
    ; 生成 500 到 1000 毫秒之间的随机延迟
    RandomDelay := Random(500, 1000)
    SetTimer(PressPageDown, RandomDelay)
}

; 按下 Page Down 键
PressPageDown()
{
    global keyHeld  ; 声明 keyHeld 为全局变量
    
    if (toggle.Value) {
        Send("{PgDn}")
        RandomDelayAndSendPageDown()  ; 继续下一次按键
    } else {
        keyHeld := false
    }
}

思路1:WebRecorder

概述:用 WebRecorder 插件,录制网络请求(WebRecorder 插件会 hook 浏览器的 XMLHttpRequest 机制),dump 出 warc (Web Archive)文件,然后再解析文件,从中提取 mime 类型为图片的文件(或者知道 URL 格式的话也可以用 URL 格式匹配)。

限制:导出的时候存在文件大小限制(Chrome 下似乎是 2G,也取决于可用内存大小),总文件大小太大的话可能会有问题。

步骤:

  1. 安装 Chrome 插件 Webrecorder ArchiveWeb.page
  2. 浏览器插件栏找到 Webrecorder,点击 Start Archiving
  3. 跳转到需要抓取的网页
  4. 打开翻页器,一直往下滚动
  5. 完成后打开插件,点击 Stop Archiving
  6. 点击插件界面内的 Home 按钮,找到刚才的 Archiving Session,点击 Download,下载一个 wacz 文件到本地
  7. 修改扩展名为 zip,将其中的 archive/data.warc.gz 文件解压出来,得到 data.warc 文件
  8. 运行如下 Python 脚本,解析 warc 文件,并从中提取图片内容
Python 解析 WARC 代码
from warcio.archiveiterator import ArchiveIterator
import os
import re

# 定义一个函数过滤掉不支持的字符
def sanitize_filename(filename):
    # 定义不允许的字符
    invalid_chars = r'[\\/:*?"<>|]'
    # 替换这些字符为空字符,或者可以替换为下划线 "_"
    sanitized_filename = re.sub(invalid_chars, '_', filename)
    return sanitized_filename

# 提取 WARC 文件中的图片、视频资源
def extract_resources_from_warc(warc_file, output_dir):
    with open(warc_file, 'rb') as stream:
        for record in ArchiveIterator(stream):
            if record.http_headers:
                content_type = record.http_headers.get_header('Content-Type')
                if content_type and 'image' in content_type:
                    # 获取资源文件的 URL
                    url = record.rec_headers.get_header('WARC-Target-URI')
                    print(url)

                    # 获取资源文件内容
                    content = record.content_stream().read()
                    
                    # 确定文件扩展名
                    ext = 'jpg' if 'image' in content_type else 'mp4' if 'video' in content_type else ''
                    if ext:
                        # 生成文件名
                        sanitized_url = sanitize_filename(os.path.basename(url))
                        filename = os.path.join(output_dir, sanitized_url) + '.' + ext
                        print("filename", filename)
                        # 保存文件
                        with open(filename, 'wb') as f:
                            f.write(content)
                        print(f"Extracted: {filename}")

# 示例用法
warc_file = 'data.warc'
output_dir = 'extracted_resources'
os.makedirs(output_dir, exist_ok=True)
extract_resources_from_warc(warc_file, output_dir)

思路2:MitmProxy

概述:安装 mitmproxy,设置浏览器代理指向 proxy,浏览时从响应中直接过滤出图片内容并保存到本地

限制:似乎没什么限制

步骤:

  1. 安装 mitmproxy
  2. 保存以下 Python 脚本为 save.py
mitmproxy save.py
import os
import re
from mitmproxy import ctx, http

# 定义文件保存目录
save_dir = "saved_media"

# 确保目录存在
if not os.path.exists(save_dir):
    os.makedirs(save_dir)

# 定义一个正则表达式,匹配Windows非法的文件名字符
def sanitize_filename(filename: str) -> str:
    # 替换非法字符为下划线
    return re.sub(r'[<>:"/\\|?*]', '_', filename)

def response(flow: http.HTTPFlow):
    # 获取响应的Content-Type
    content_type = flow.response.headers.get("Content-Type", "")

    # 判断是否为图片或视频类型
    if "image" in content_type:
        # 获取文件扩展名
        ext = content_type.split("/")[-1]

        # 从请求路径中尝试提取文件名,如果路径中没有文件名,则使用flow.id
        url_path = flow.request.path.split("/")[-1]
        if "." in url_path:  # 判断路径中是否包含文件扩展名
            raw_filename = url_path
        else:
            raw_filename = f"{flow.request.host}_{flow.id}.{ext}"

        # 对文件名进行清理,确保符合Windows的文件名要求
        safe_filename = sanitize_filename(raw_filename)

        # 确定完整的保存路径
        filepath = os.path.join(save_dir, safe_filename)

        # 记录日志,打印保存的文件名和URL
        ctx.log.info(f"Saving file: {filepath} from URL: {flow.request.url}")

        # 保存文件内容
        with open(filepath, "wb") as f:
            f.write(flow.response.content)

        # 打印成功保存的日志
        ctx.log.info(f"Saved {filepath}")
  1. 运行 mitmproxy
# 需要先 cd 到 save.py 所在的目录
# 可以添加 --mode upstream:http://localhost:7890 来指定上游代理
mitmweb -s save.py
  1. 设置浏览器,指向 mitmproxy 代理(可能你需要 proxy switchyomega
  2. 跳转到需要抓取的网页,打开翻页器
  3. 滚动到底部,且所有图片加载完成后,需要抓取的图片应该都已经保存到本地了

如何手动 squash

2024-10-07 13:56:00

最近帮解决了一个因为提交流程不规范导致的诡异 Git 分支问题,特此记录下,以备后用。

背景:主干分支 main,特性分支 feat,在特性分支上开发特性的时候,多次合入了主干分支(仅快进,没有合并冲突);模拟的 Git 历史如下图所示,其中 m* 是主干提交,f* 是特性分支提交

* 5fb94b2 (HEAD -> main) m4
| * 83c6299 (feat) f3
| *   ff5c5af Merge branch 'main' into feat
| |\
| |/
|/|
* | 1698c3c m3
* | 05199a8 m2
| * 68a7e1d f2
| * eb5b169 f1
|/
* ae79b7c m1

问题:如何把特性分支上的所有提交合并为一个提交?(类似于 squash 的 Merge 策略,但是手动做到这一点)

思路:找到主干和特性分支的第一个分叉点,以此为基准生成 patch,然后在一个新分支上 apply patch 得到一个纯净的 commit。

(非人工写作提示:以下是 LLM 根据我自己的笔记生成的内容。欢迎将反馈贴在评论区,这将决定我以后是否会更积极地使用 LLM 进行创作。)


在日常开发中,我们经常会遇到这样的问题:由于开发流程、代码审查或其他原因,特性分支(Feature Branch)和主干(Main Branch)上的提交记录混杂在一起,难以管理。为了让代码历史更为清晰、整洁,我们通常需要手动进行 squash 操作,将特性分支上的多次提交合并成一个提交点。

这篇文章将带你一步步了解如何手动进行 squash 操作,并确保不丢失任何数据。这种方法适用于已经有部分提交合并进主干,并且历史记录较为复杂的场景。

场景问题

假设你正在开发一个新功能,但在开发过程中,特性分支 feat 和主干分支 main 上的提交混杂在一起。这样一来,不仅使代码历史难以追溯,还会影响代码审查和后续维护。因此,我们希望将 feat 分支上的所有提交整合成一个提交点,并保持代码历史的清晰度。

前置准备工作

在进行操作前,我们需要做一些准备工作,确保不会出现数据丢失的风险:

  1. 创建备份分支并推送到远程: 在特性分支 feat 上创建一个备份分支,并推送到远程仓库,以确保操作过程中的数据安全。

    git checkout feat
    git checkout -b feat-backup
    git push origin feat-backup
    

    这样,即使后续操作中出现意外,我们依然可以通过 feat-backup 分支恢复数据。

步骤详解

1. 确定变更点

首先,我们需要找到 feat 分支和 main 分支的最后一个重合点(也就是 main 分支中包含但 feat 中不包含的最后一个提交)。这样我们就可以清晰地识别出哪些提交属于 feat,而哪些提交是混杂进来的。

如何找到变更点?

  1. 使用以下命令,找出 feat 分支中第一个与 main 分支分叉的提交:

    git log --oneline main...feat --reverse --pretty=format:"%H" | head -n 1
    

    此命令将列出 feat 分支中所有提交的哈希值(commit hash),并按照时间顺序排列,其中 head -n 1 取出第一个分叉点的哈希值,记为 {first_diverge_commit}

  2. 然后使用以下命令查找 mainfeat 最后一个重合点的哈希值:

    git rev-list --parents -n 1 {first_diverge_commit} --reverse | head -n 1
    

    取命令结果的右边部分,这就是 mainfeat 的最后重合点,记为 {last_share_commit}

2. 计算差异(Patch)

现在我们已经知道了 main 分支和 feat 分支的分叉点和最后重合点,我们可以提取出 feat 中相对于 main 的所有变更。

使用 git diff 生成差异文件:

git diff main feat > ~/my_patch

注意: 请将 patch 文件存放在仓库目录外,例如 ~/my_patch,因为后续执行 git reset 时会重置仓库目录内的所有文件,导致 patch 文件丢失。

3. 回溯到最后重合点

接下来,我们要将当前 feat 分支的状态回退到 main 分支的最后重合点。

git reset --hard {last_share_commit}

该命令将 feat 分支的状态重置到 {last_share_commit} 提交。注意: reset --hard 会丢失所有当前分支的更改,因此确保之前的 patch 文件已备份。

4. 快进主干(Fast-Forward Merge)

现在,我们需要让 feat 分支快进(fast-forward)到主干 main 的最新状态。

git merge main --ff-only

--ff-only 参数表示如果不能进行快进合并,则不会合并。这一步确保 feat 分支的历史记录与 main 分支保持一致。

5. 应用补丁(Apply Patch)

回退和快进操作完成后,我们将 feat 分支上原本存在的所有提交变更(即 patch 文件)重新应用到当前分支。

git apply ~/my_patch

6. 提交变更并推送

现在,我们可以创建一个新的提交,将 feat 分支的所有变更整合到一个提交中。

git add .
git commit -m "Squashed all feature changes into a single commit."

接着,将 feat 分支强制推送到远端,以确保远程仓库与本地分支一致:

git push origin feat --force

总结

通过上述操作,我们成功地将 feat 分支的所有提交整合成了一个提交点,并且与主干保持了清晰的历史记录。完整步骤如下:

  1. 找出特性分支和主干的分叉点与重合点。
  2. 生成 patch 文件保存变更。
  3. 回溯到重合点并进行快进合并。
  4. 应用 patch 文件,合并所有更改。
  5. 提交合并后的变更,并推送到远端。

通过这种手动 squash 方法,你可以灵活地调整提交历史,确保代码库的整洁度和可读性。

勘误

LLM x 书签收藏:摘要 & 全文索引

2024-10-07 13:52:00

背景

网上冲浪的时候,经常会遇到一些有趣的文章或者网站,让我有收藏起来以备后用的冲动(虽然绝大部分情况下都没有再用过)。然而一个人收藏未免有些太孤单了,因此自从 2021 年 5 月以来,我一直在使用一个名为 osmos::memo 的书签插件,将我的收藏直接记录到一个公开的 Github 存储库。这个插件的工作原理很简单,首先设置好 Github 的 token,然后每次点击收藏按钮都会在浏览器里临时 clone 一次,追加新收藏的条目到文件顶部,生成 commit 并提交,然后推送回 Github。但是这样简单的工作流程也十分有效,除了 token 过期的时候需要手动续期(过期前有 Github 自带的邮件提醒,所以基本上不会拖到最后),没有什么可以出错的空间,近三年半下来也已经帮我积累了 800+ 条目了。

问题

然而目前的书签收藏流程中,依然会存在一些问题。

  1. 书签指向的 URL 可能不再存在(例如某个博客的主人决定不再续费域名,或者是做了链接格式的调整),导致成为悬空的死链接
  2. 目前的记录项只有书签的 URL、标题和可选的标签(而且我打标签的习惯不太好,光靠标签基本上不太能找到),导致查找的时候如果对关键词记忆不清楚,很有可能找不到
  3. 书签里一大部分是长文章,时间一久很有可能忘记内容,如果只是临时找些东西,通读一次又略微有些费时费事,导致查找&引用效率下降。

解决

为了解决这些问题,我建立了一个新的存储库 bookmark-summary。这个存储库可以视为现有书签存储库的辅助数据,其中包含了新增书签的 Markdown 格式全文、列表摘要、一句话总结,和现有存储库之间通过 Github Actions 联动。其工作原理如下:

  1. 我通过书签插件,在现有的书签存储库中新增了一个条目
  2. 代码提交到主干,触发名为 summarize 的 Github Actions(yml 工作流文件
  3. Github Actions 执行,首先 checkout 书签存储库和摘要存储库,然后执行 process_changes.py
    1. 首先解析书签 README.md 文件,找到最近新增的条目标题和 URL
    2. 将 URL 保存到 Wayback Machine
    3. 输入 URL,使用 jina reader API 获取网址的 Markdown 全文,并保存到 YYYYMM/{title}_raw.md
    4. 输入 URL,使用 LLM 生成列表摘要(prompt 在 summarize_text 函数 link
    5. 输入列表摘要,使用 LLM 生成一句话总结
    6. 将列表摘要和一句话总结保存到 YYYYMM/{title}.md效果示例
    7. 更新摘要存储库的 README.md,增加到摘要文件的链接
  4. Github Actions 提交变更到摘要存储库

这里的主要代码基本都是 Claude 和 GPT4o 写的,人肉做了一些小调整。后面随着使用又逐步发现了一些 bug,最近还用 o1-mini 修复了一个,算是真切感受到了 LLM 对生产力的巨大提升。目前摘要生成用的是深度求索的 deepseek-chat,便宜是真便宜(输入 1元/1M token,输出 2元/1M token,在这个场景下的成本基本上是每个月1元不到),效果也还算可以。

未来

最后是一些已知问题,以及未来可能的优化方向。当然和其他所有项目一样,欢迎 fork & PR。

  1. 列表摘要质量:可能是 prompt 的问题,列表摘要倾向于每个大点下面只列两个小点,且没有充分合并需要合并的论点;可能需要考虑进一步优化 prompt,或者换用其他模型(不过我拿便宜的模型都试了一轮,基本上都存在类似问题)
  2. 数据结构化:目前摘要存储库下有个简单的 data.json,但是核心的摘要和全文内容依然是 Markdown 存储的,而不是 JSON 这类程序友好的结构化存储。可能需要考虑在 Markdown 之外另外维护一个 JSON,以备未来的查询。
  3. 代码整理和重构:目前所有逻辑都混在一个大的 Python 文件里,修改和测试起来都很烦人(实际上没有特别好的办法手动测试,目前都得靠手动注释掉部分代码)。未来一个考虑是做重构(o1-mini也给出过比较好的重构结构)+补充测试;另一个是改进书签存储库和摘要存储库的交互方式,例如通过读 git log 或者是明确传递最近书签条目的方式来触发摘要生成,而不是靠目前读文件对比的方式
  4. 向量搜索:目前虽然原文和摘要都存下来了,搜索却还是只能靠基本的文本匹配;可能可以考虑接个 embedding 模型自动生成下嵌入,存到一个 SQLite 数据库(或者用各种向量数据库 as a Service);主要是查询的时候也得生成 embedding,英文还有小模型可以搞,中文的模型都太大了,没法直接在前端跑不依赖后端服务,这里还得再仔细想想。
  5. 自动生成每周周报:既然现在书签有时间信息,可以考虑每周新增的书签+原文+摘要全部往 LLM 扔,自动生成一个每周摘要,放在 Github Release 里(不过不知道有没有人愿意看就是了) (已完成,参见 Releases,实现见 build_weekly_release.py,代码主要由 o1-mini 实现)
  6. 改用更现代的工具链:例如 uv,以及把依赖写在 Python 代码头部(PEP 723 Inline Script Metadata)?

我也想要

可以参考以下步骤,在自己的 Github 账户下部署一套类似的系统。(根据回忆写的,所以可能不太详尽)

  1. 参考 osmos::memo 的指引,初始化书签存储库(我的叫做 bookmark-collections),安装浏览器插件,并连接到 Github
  2. 新建一个摘要存储库(我的叫做 bookmark-summary)
  3. process_changes.py 添加到摘要存储库,用实际的存储库名修改 BOOKMARK_COLLECTION_REPO_NAMEBOOKMARK_SUMMARY_REPO_NAME;如果需要的话,可以调整 summarize_textone_sentence_summary 中的 prompt
  4. 回到书签存储库,将 bookmark_summary.yml 添加到 .github/workflows/bookmark_summary.yml,用 Github账号/摘要存储库名 替换 27 行 jerrylususu/bookmark-summary
  5. 新建一个 PAT(Personal Access Token)
    • 入口:Github 主页 - 右上角 Settings - 左侧列表底部 Developer Settings - 左侧列表 Personal Access Token / Fine-grained Tokens - 右侧 Generate New Token - 验证密码
    • Token Name: 随便写
    • Expiration:可以长一些,但是不能超过 1 年
    • Repository access:选 Only select repositories,然后在下面选中自己的摘要存储库
    • Permissions:点开 Repository Permissions,找到 Contents,选择 Read and write;其他不用动
    • 点击底部 Generate Token;Token 只会显示一次,复制下来保存好
  6. 回到书签存储库,添加密钥到环境变量
    • 入口:摘要存储库 - 顶部 Settings - 左侧 Secrets & Variables / Actions - Repository secrets - New Repository Secret
    • 需要添加 4 个(其实有的可以放在 Environments 里,不过这里我为了方便先全放到 Secrets 里了);冒号前面的是名字,冒号后面的是内容
    1. PAT :第 5 步生成的 token
    2. OPENAI_API_MODEL : 模型名,如 gpt-4o-mini;如果像我一样用 deepseek 则填写 deepseek-chat
    3. OPENAI_API_KEY : API key,通常以 sk- 开头
    4. OPENAI_API_ENDPOINT : 模型 API 地址,留空默认用 OpenAI 官方;可以填中转站;用 deepseek 则填写 https://api.deepseek.com/chat/completions
  7. 至此应该配置完成了。可以用 osmos::memo 扩展添加一个书签试试,观察书签存储库中工作流是否正常运行,摘要存储库中是否生成了对应的摘要。

项目

2024-08-31 23:45:30

时间 类型 项目 链接 状态
2024 个人 cococlock:模拟 Apple Watch 的 Gradient 表盘(JS) WebApp Github ✅在用
2024 个人 show-bit-flag:位掩码可视化工具(JS) WebApp Github ✅在用
2024 个人 day-tracker:看看今天还要工作多久(JS) WebApp Github ✅在用
2023 个人 MainOnly:在网页上隐藏无关元素,只显示主体内容的小 bookmarklet (JS) Intro Github ✅在用
2023 个人 PaddleOCR-json Java API:PaddleOCR-json 的 Java 封装(Java) Github ✅在用
2022 个人 航班熔断模拟器(Vue) Github ⛔已下线
2022 个人 Bangumi Takeout:从 Bangumi 中导出自己的标注记录(Python) Github Colab 🛠️仅维护
2022 个人 Gojuon Quiz:日语五十音图记忆和测试小工具(Vue) WebApp Github ✅在用
2022 个人 Crafting Interpreter 实现和个人笔记(Java, C) Github 🚫不适用
2022 个人 APIJSON 示例项目(Java) Github 🔄他人维护中
2021/12 个人 joplin-vaccum:清理笔记 Joplin 软件中的孤立图片(Python) Github 🛠️仅维护
2021/12 课程 队列论:事件驱动的队列模拟器(Python) 非公开 🚫不适用
2021/12 课程 分布式系统:课程笔记,Raft/KVRaft 实现(Go) 博客 笔记 🚫不适用
2021/10 课程 描述逻辑模拟器 ALCQ(Python) Github 🚫不适用
2021/4 课程 毕业设计:用于多媒体教学的实时字幕识别和分发系统
语音识别 + websocket + Spring Boot
非公开 🚫不适用
2020/5 课程 软件工程:Github Fixit
给 Java CLI 库 Picocli 和 Java 服务器框架 Spark 修了一些 bug。
B站 Github PR 🚫不适用
2020/2 实习 Lustre File Stat Gather
一个扫描 Lustre 文件集群元数据得到统计信息(大小分布、日期分布等)的工具。
非公开 🚫不适用
2019/12 课程 面向对象程序设计:ArchOJ:一个面向教学的在线测评系统 非公开 🚫不适用
2019/12 课程 计算机安全:联创打印系统安全性研究
调查了校内联创打印系统的安全问题,如绕过计费、登入他人账号、恢复历史打印数据等
非公开 🚫不适用
2019/12 课程 计算机网络:Spanning Tree Routing
用 Python 实现生成树路由,并用 SDN 模拟
Github 🚫不适用
2019/12 课程 生成式新媒体设计:Processing 动画 Github 🚫不适用
2019/7 课程 NUS Summer School: EmojiCam
Best Project In Cluster
openCV + 情绪识别 + FaaS 修图
Poster 🚫不适用
2019/6 课程 计算机组成原理: CPU in MIPS(Verilog) Github ⛔不再维护
2019/1 课程 数字逻辑: Digital Clock(Verilog) Github 🚫不适用
2019/1 个人 阵营九宫格生成器 & 逐渐离谱生成器 WebApp Github ✅在用
2018/9 个人 MailStat:学校邮箱邮件分析 Github ⛔不再维护
2018/6 个人 宿舍舍友预约系统 非公开 🔄他人接手运行中
2018/5 课程 Java 2: 机上娱乐系统 Github 🚫不适用
2018/1 个人 超简单视频播放器 (Android) Github ⛔不再维护
2017/12 个人 某基金会捐款查询系统 非公开 ⛔已下线
2017/9 课程 Java 1: 答疑预约系统 非公开 🚫不适用
2017/6
个人
Project SFLS 🎶 Github ⛔不再维护

在 Devtools 里触发前端组件的内部状态更新

2024-08-31 22:54:10

这个标题大概不太好理解;以下是对我遇到的问题,及我的解决方案的描述,在获得了相关上下文后,这个标题可能会稍微好理解一些。

背景:在我的工作中,时常需要使用一个内部的日志查询平台。在使用时,需要先指定日志的开始和结束时间,默认情况下开始时间会被设置为今日的0点,结束时间则被设置为今日的23:59:59。虽然大部分情况下默认值都足够了,但是有时我需要调整时间范围,例如,选择为昨天,或是选择为某个 unix timestamp / yyyy-MM-dd hh-mm-ss 的前后一分钟。但无论是从界面上选择日期,还是手动输入时间都有些麻烦。我想半自动化这一过程,例如写一些 userscript 来改善时间范围的选择体验。

问题:我需要在无法接触/修改前端源代码的情况下,用 js 修改这个日期选择器的值。

从类名中不难发现,这个日期时间选择器底层实际上就是 Element UI 的 DateTimePicker。然而如果直接修改对应的 input 的 value,并不能满足要求,因为这只修改了表现层的输出,Vue 组件中的内部状态实际上没有更新(也可以从点击查询按钮后发出的网络请求中验证)。正确的做法是用 js 在对应的元素上触发合适的事件,让组件像处理用户输入那样处理我们的请求。那应该触发怎样的事件呢?

有几个思路可以找到对应的事件:

  1. 使用 Chrome Devtools 自带的 monitorEvents :首先用右键-审查元素在 DOM 树中定位到 input,然后右键"存储为全局变量",会保存到 temp1,再在控制台输入 monitorEvents(temp1) 就可以观察到该元素上的所有事件了。然后像正常使用一样操作下选择器,可以看到触发的事件和参数。(mouse 相关事件会有很多坐标更新,杂音比较大)
  2. 打开 Github 找到这个组件对应的源码,相关的 @input / @change 等方法也能说明该组件会处理的事件类型

但作为现代开发者,首先当然还是先问问 GPT 了;GPT 给了一个看起来很靠谱的 script,节选如下。其中基本把组件可能处理的事件都触发了一次。

inputElement.click();
inputElement.value = '2024-08-31 12:34:56';
var inputEvent = new Event('input', { bubbles: true });
inputElement.dispatchEvent(inputEvent);
var changeEvent = new Event('change', { bubbles: true });
inputElement.dispatchEvent(changeEvent);
inputElement.blur();

这个 script 在官网的 demo 上的确可以用,但是很不幸在我的内部工具页面上并不行,会出现一个奇怪的 Uncaught TypeError: Cannot read properties of undefined 错误,点击调用栈的话只有 minified 的代码,完全看不出来是什么问题。到这里似乎陷入了僵局。然而进一步的实验发现,似乎这个问题只会在页面首次使用的时候出现;如果我手动先选择过一次日期时间,再用这个脚本就可以设置了。看了下组件源码,注意到组件在更新内部状态时,还会同步去更新 picker(弹出的日期弹框)里的值,是否可能是这个问题?有了这个模糊的思路之后,我再次开着 devtools 开始验证我的猜想。页面首次加载之后,DOM 树中并没有 picker 对应的节点,此时用脚本设置会失败;然而手动操作时,picker 节点会被创建,设置完成后被隐藏(但依然在 DOM 树中);再次运行脚本,设置日期时间值成功了。看来的确是 picker 在脚本运行时没有正确被初始化导致的。

下一步就清晰一些了,只需要想办法在脚本操作之前,保证 picker 已经被初始化就好了;继续尝试了源码里的各种事件,最终发现可以用 focus 事件触发 picker 创建,用 Esc 可以让 picker 消失。(这一步其实试了很久,而且中间有几次把 bubbles 写成了 bubble 导致一直触发不成功)

最后得到的可以正常工作的脚本如下。虽然很丑陋,但是至少能用了…

inputElement.dispatchEvent(new Event("focus", {bubbles: true}));
inputElement.click();
inputElement.value = '2024-08-31 12:34:56';
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
inputElement.dispatchEvent(new Event('change', { bubbles: true }));
inputElement.blur();
inputElement.dispatchEvent(new KeyboardEvent("keydown", {key: 'Esc', keyCode: 27, bubbles: true}));

Obsidian 与工作日志

2024-08-18 15:30:00

虽然从开始工作到现在已经有两年多了,但大部分时间里我需要同时跟进的事项并没有那么多,复杂度也没有太高,基本上不需要太多记录就可以完成。但是最近几个月以来,手头工作的数量和复杂度都急剧上升,完全依靠大脑跟进已经逐渐不可能了。在此背景下,我开始尝试用 Obsidian 搭建自己的工作日志系统,也读到了其他人的一些分享(如 Use A Work Journal To Recover Focus Faster And Clarify Your Thoughts)。目前我的工作日志系统已经正常运转大概三个月了,大部分坑都已经填平,也成为了我工作中不可或缺的一个部分。以下是我自己目前的一些经验,希望分享出来能帮到各位读者。

工作日志能解决的问题

  1. 在多个任务间切换而不丢失信息:随着跟进任务数量的增加,将所有任务相关的信息记忆在大脑里越来越困难,然后会发现越来越多的时间被花费在信息寻找上:这个任务的代码在哪个分支上?我今天要交付的文件应该找谁要?这个项目的最新结论是上次谁在哪个群拍的板?有了工作日志之后,每个任务都有自己独立的条目,只要找到它,相关信息就能立刻获得。
  2. 记录每一步尝试:有些时候第一次尝试就能成功,但更多时候并非如此。通常需要很多次修改、调试和观察,才能确认自己是否在正确的方向上前进。最终提交的代码或文件只反映了最后成功的结果,中间的探索过程却完全丢失了。有了工作日志之后,一切中间过程都能被记录和回溯。
  3. 快速复用SOP以保证关键任务的可重现性:探索性的任务很有趣,但也有一部分任务是事务性的:目标明确,步骤清晰,也做过很多次了;但是步骤数量增加和操作过程的复杂度提升,都会让某一步骤遗忘/未能按照预期完成的概率增加;工作日志让维护和应用SOP(Standard Operation Procedure,标准操作流程)更简单,只要每次遵循就能避免出错。(当然更好的选择是完全将事务性工作自动化,让人不用参与,然而这并非总是可行/经济)
  4. 阶段性总结时有话可说:在大厂打工,(周|月|季|半年|年)报难以避免,然而很多工作都很琐碎,一个周期过去了可能发现自己甚至说不出来做了什么;工作日志让回溯历史更加简单,避免了无话可说的窘境。

我如何使用工作日志

什么任务需要建立工作日志

目前我的标准是预估完成时间,超过 5 分钟的任务就值得建立一条工作日志了。在我目前的工作流中,我通常会在一个 4K 分辨率的屏幕上操作,左侧 70% 是我当前的核心工作区(如浏览器/代码编辑器),右侧会开三个窗口,从上到下分别是 Apple Notes(临时任务列表)、CudaText(草稿纸/scratchpad)、Obsidian(工作日志)。当我收到一个任务(可能是电话/IM消息/当面通知)后,我会先判断该事项完成所需的时间;如果预估可以在 5 分钟内完成(简单的配置修改/信息收集表填写/告警单处理),那就会放在 Apple Notes 里作为一个新的待办项;如果预估需要 5 分钟或者更长(bug 调查/开发需求),那就在 Obsidian 里创建一个工作日志条目文件。当然预估的时间可能不准,如果实际开始做的时候发现比我预估的时间更长,我也会把这个任务从 Apple Notes 的代办项提升为一个 Obsidian 日志。

工作日志模板

之前我是的每个工作日志都是从零开始,然而随着日志的逐渐增加,我观察到自己在每个日志初始时写下的信息有共同之处,于是从中提取建立了模板。目前我使用的模板很简单,只是有一个 Markdown 表格,描述了这个任务的常用关键信息,其中包含以下的 key:

使用工作日志

从模板建立工作日志并填充基本信息后,这个工作日志就可以使用了。

特殊文件

除了每个任务特定的日志之外,我还维护了一些特殊文件,每个都有自己的特定用途。

相关的 Obsidian 插件

虽然工作日志的存在本身就是有意义的,但是和一些 Obsidian 插件配合可以更方便。

暂未解决的问题

最后是一些我目前还没有完全解决的问题,如果有思路欢迎分享。

读书记录《时间贫困》

2024-08-12 21:36:00

书名:《时间贫困》

评价:7/10;很短的书,快的话一小时就能读完;了解到了一些和自己认知之外但符合自己真实感受的观点(e.g. 完全躺平未必会快乐);可能会试试书中描述的行动。

版本:微信读书


观点

行动

惠州海边躺平记录

2024-08-12 17:08:00

基本信息

日期:2024/8/10 ~ 2024/8/12

原因:因为年假快到期了,想着赶快花掉;之前在网上看到过惠州躺平的视频,于是也想来试试

地区选择:看了下小红书的帖子,主要推荐的是泡泡海这一边,另外似乎还有几个海边的点,但是都属于旅游区,价格和生活基建上会贵一些;然后在美团找了,的确有做相关民宿的,价格大概是200/晚;最后选了一家在 海悦长滩花园 的,宣传是阳台可以看到一线海景


第一天下午更新

前往:8/10 上午出发,高铁大概半个小时就能到(24元,深圳北-惠阳);但是到了之后还要再打车,高铁站打车过来大概是 50 元(比高铁票还贵一些了);开到附近的时候可以很明显附近除了几个大的看海小区,剩下的都是城中村和农民房,还是有一些生活气息的;下车后找到进楼的入口花了点时间,4部电梯 + 31层,需要稍微等一下,但是也还可以接受;等电梯的时候看了下,住户应该都是暑假或者周末出来玩的,不像是长期住户;到了之后直接电子门锁密码开门就行

房间:很简单的一房,有卫生间,床,阳台,沙发;阳台上的确是一线海景,因为楼层足够高,虽然附近有其他楼遮挡,还是可以看到很多大海;不过可能是之前的期待值太高,第一眼看起来并没有预想的那么震撼(后来待久一些,其实还是挺耐看的);缺点是楼下在挖路,机械的声音比较嘈杂;房间内部就是类似酒店的陈设,没有什么特别好介绍的;热水器是即热式的,不过一开始进来的时候跳闸了,和房东联系了之后才找到开关打开。;在阳台上望出去还能看到远处的港口和工业园区,看大烟囱喷白烟也挺奇妙的。;另外还有个缺点是阳台可以看到附近的入户走廊(那也就意味着入户走廊上也能看到你),所以不是完全的隐私的阳台

海滩:在房间里稍微休整了一下,然后决定下楼去海滩看看;大概一两百米就能走到海滩入口;上一次去海滩感觉已经是很久之前的事情了(硬要说的话应该是本科某次班级活动,在海边租了别墅轰趴),下海游泳就更久了(初中?),虽然去深圳湾公园倒是去的很多(上一次应该是在24年3月);这次没有带泳衣,也不打算游泳,只是在海边沙滩上走走感受一下就足够了。海水漫过脚尖,然后再慢慢流回去,有种说不出来的舒适和放松感;海浪的声音也很有规律。附近的海滩还挺长的,感觉上可能大概有五六百米(后来地图看了下,大概是八百多米),第一段是普通的海滩,没有防鲨网但依然有人尝试下海泡着(然后被救生员骂回岸上了);然后是一段游船和摩托艇用的海滩,还有一些浮桥作为码头;再往后是可以下海的海滩,人很多,有种泡饺子的感觉(但是肯定没有大梅沙那么多人);再往后是一段人少的海滩,能真正感受到沙子的细腻,更类似于影视作品中细沙的感受(前面的海滩感觉有很多碎而尖的东西,不确定是贝壳还是啥,有点磨脚;另外还有烧烤竹签啥的,得时刻注意脚下);走到头是个叫虎洲岛的小岛(其实和陆地还是连起来的),用铁丝网封起来,还有告示牌说不能进入,但是依然有人挖了洞钻进去(里面还不少人);走到头之后就慢慢往回走,偶尔就干脆停下来让腿感受海浪的冲刷;后来来了几波大浪,短裤也彻底湿了,就开始慢慢走回去;前半段的时候天气还有点阴沉,但后面偶尔出了点太阳,能看到阳光把浪花高亮起来;总之是一次很舒服的体验

周边:楼下有好几家便利店,稍微走了下也有不少快餐,不过物价比深圳的城中村稍贵(可乐 4.5,快餐 20~25),长期居住的话吃饭的开销可能比较严峻;外卖基本也能完全 cover,价格也就是一般城区外卖的价格;旅游区当然还有不少海鲜餐馆,不过这次海鲜不是重点就先跳过了;另外靠近海滩还有不少推车小贩,没问价格,但是至少吃的选择上肯定是够多了

房租:楼下有托管中心,尝试问了下,大概是每月一千的价格,年租价格相同(长期躺平的话房子价格有些贵了,相比于其他四五百一个月的地点来说),但是海景和层高是个很大的优势


第一天晚上更新

夕阳西下的时候,决定还是去附近走一走,顺带想想晚饭去哪里吃;下楼之后大概转了下,走到镇上去又走回来,一路上基本都是海鲜餐馆,而且都是大份量的饭菜,面向的都是两人或者以上的,一个人吃的话其实很难找到比较好的选择(例如看了几家餐馆的菜单,海胆炒饭起步70+,一般的蛋炒饭也要三四十);一个人吃的话,基本上要么是海滩附近的推车小贩,要么就是类似于兰州牛肉面、沙县小吃这类各处都能吃到的食物了;最后还是选择在海滩附近的推车小贩搞了点吃的:蛋炒饭20(还不错,虽然已经要求了微辣,但是对我来说辣椒还是放多了);蚝烙15(感觉和之前听说的不太一样,实际感受是生蚝没放多少,倒主要是面饼了,感觉不是很值);还在附近的摊位买了烤肠x3 10元(纯淀粉肠,而且里面感觉没热透;吃了两个,剩下一个放弃了);考虑明天以外卖为主吧,或者就楼下随便吃点;另外回公寓的时候发现楼下的两家便利店,可乐的价格相差一块钱(5元vs4元),买东西的时候还是得小心点;最后买了水、可乐和明早的面包,就回房间了,晚上还是走了不少的,少说得有两公里了(回来查了下地图是2.2公里),附近也就是正常县城的基建水平,长期定居的话也不是不能接受。


第二天上午更新

晚上睡觉的时候发现了一些小问题;一是外面走廊的灯太亮了(之前也提过阳台可以看到外面走廊),必须要拉窗帘才能睡得着;二是房间的隔音不是非常好,虽然可能隔壁房间没有住客所以没有什么声音,但是外面走廊的声音(例如行李箱滚轮)可以听的很清楚;三是早上的时候楼下工地的声音比较大,会被吵醒(看了下是七点多),未来如果还要来的话得考虑带上耳塞;四是床附近没有插座,所以只能先把手机的电量充满再放到床边用,不是很方便;五是晾衣服的地方不太够,目前暂且是挂在椅子和桌子上晾的;六是因为角度的问题,需要呆在阳台的一个死角,才能不被对面走廊看到。

昨晚大概是两点多睡的(应该是晚上喝了可乐的缘故,咖啡因太强睡不着);七点多醒了一次,最后实际起床是十一点左右。


第二天下午更新

在房间里摸了一会到了十二点多,远处的天空渐渐阴沉了,也能不断听到滚滚雷声,但是就是没有雨;过了会到十二点半,倒是有些小雨,能从阳台玻璃上看到雨滴,然而感觉并不算大;看了下手机大概十分钟后就停了(虽然惠阳气象台发了预警,但是估计主要是城区,海边影响比较小?);快一点多决定还是下楼去搞点吃的;下了楼,决定在昨天看到的沙县吃个午饭,就点了比较简单的鸡蛋火腿炒米粉 18元;上的倒是很快,也就是一般水平,油多了点(连吃两顿炒粉还是不太能顶得住)

吃完了决定再去海滩转转。这次一开始在海边走反而不太适应了,明显感觉有点扎脚,怀疑是类似于塑料碎片之类的东西,没有被充分打磨过,走了一段反而有种上刑的感觉了;再往前稍微走了一点到靠近游船码头的地方,沙子的感觉才明显好起来,更接近期望中的顺滑的细沙的感觉;再往前走,景象和昨天没什么差异,船还是那些船,人也是一样的多(甚至感觉比昨天更多了,不知道是否错觉);不过摩托艇似乎降价了(昨天六七十2个人,今天只要五十了,可能是因为七夕的缘故?);在沙滩上站了一会,感觉越来越晒了,才发现乌云早就飘走了,烈日重新照耀起来;在往回走的时候,突然想下海试试看;因为是中午,海水并不会感觉很凉,甚至还有些微微的暖意;不敢走的太远,就在可以站着碰到底部的地方,让身体随着海浪慢慢漂浮;体感上其实和之前在海边的感受没有什么不同,只是一个人的话可能会感觉更自由一些?;虽然想放松下来,不过想到自己一来不会游泳且没有游泳圈,二来衣服还在沙滩上没人看着,所以其实没法完全放松;泡了几分钟,感觉也体验的差不多了,就决定回去了。

快回到楼下时,在便利店买了点水、吃的和雪糕,居然花了25元,果然是景区价格;回到房间有空调的确是太爽了,赶快把衣服脱了开始洗澡,再把衣服也泡了水,尝试把衣服上沾到的沙粒洗干净,然而发现水下去之后盆底还是有一层薄薄的沙,估计临时处理下还是冲不干净的,得回家再做处理;阳台上已经完全放晴了,不过这会外面还是太热了,决定把阳台上的椅子搬到屋里,边望着海景边打字(还开着空调),实在是舒服;海面上能看到游船和摩托艇,不过我是没什么兴趣就是了,下次有机会的话再来体验吧。


第二天晚上更新

落日时分决定把电脑搬到阳台上,边看着夕阳边刷着视频和网页,感觉也十分宁静;落日下去之后已经是七点多了,决定去找点东西吃;之前美团上看到了楼下有烧烤,于是决定去试试;点了正常的一人份(鸡蛋炒方便面13 牛肉串x2 10 羊肉串x3 10 韭菜x1 3 金针菇x1 3),价格39元,也是一般正常烧烤的价格,味道的话还可以;吃完了回去海滩旁边看了下,原来晚上海滩上也是有人的(不过海滩上的灯不是很亮,感觉上不是很安全);附近买了根烤肠5元,又在楼下便利店买了水,就准备回房间了;没有什么特别值得记录的


第三天上午更新

今天睡得比昨天好一些;7点多醒了一次,把窗帘拉开了(不过实际上从床头的位置看不到什么海景,还是得站起来才能看到);然后又继续睡了;10点左右一阵雷声彻底惊醒了,赶快起来先把衣服收掉了,这时雨云还在远处,昨天可以看到的远处工业基地已经看不到了;收完衣服雨也接近了,小镇都看不太真切,整片天空都被染成了朦胧的均匀的灰色;虽然天气预报写的是大雨,不过实际在阳台上,有屋檐的遮挡,实际上感觉会类似于细雨;海上依然有船,摩托艇倒是全都消失了;决定把电脑搬到阳台上,边听雨边刷刷新闻;后来发现闪电越来越接近,决定还是转移到屋内了


第三天中午更新(等车时)

快走的时候才发现原来定的是下午1:48的车,然而退房时间是12点,从海边到高铁站的车程大概40分钟,所以得在高铁站等大概一个小时;试图看了下有没有可能改签到更早的班次,然而都已经售罄了,遂放弃,反正早到了多等等也没啥问题,别赶不上就好;收拾了下东西之后,重新检查了一次所有东西都带走了,就退房了;然后才发现保洁一直在外面等着(11点半的时候就来敲过一次门了);然后就是下楼打车,高德等了几分钟没打到,换了滴滴才打到(可能提前预定的话会更方便一些);车开过来大概八分钟,到高铁站的时候差不多12:55;本次的高铁居然前面晚点了几分钟,估计出发也要晚点了(13:45->13:52)


结论

评价:7.5/10

总体上来说还是一次挺满意的旅行,完全实现了之前定下的放松+看海的目的。房间虽然不大,但是也干净够用,阳台上也的确能看海。中间还去沙滩上走了两次,还去海里泡了下。吃的也还行,以旅游价格来说的话,也吃了烧烤和蚝烙。(很多店铺都是以家庭或者团体为单位来定菜单的,没有太多一人吃的店)。

然而对躺平长居来说的话,可能不是一个太好的备选;在阳台上看海虽然很棒,但是主要也是在前面几天,后面很快就会腻了;房间比较小,也没有什么事情做,基本上也就是上网和刷视频,完全提不起兴趣做任何“有用“的事情(不过可能想要做有用的事情就已经不是长居躺平了);房价对躺平来说稍微有些贵了(约1k/月),主要的溢价应该都在海景上了,距离火车站也比较远,内陆城市相同价格应该能找到更靠近市区/交通枢纽的躺平地点。

另外本次的体验也让我暂时明白了一点,目前的自己估计还是处于”卷也卷不动,躺也躺不平“的状态。虽然这个周末自己的经历,已经几乎完全接近理想中的躺平了,但是感觉上也就前一两天能够享受,后面有一种饱和(或者说电已经充满了)的感);对我来说偶尔来充充电还是可以的,但是完全放松下来什么都不做,感觉还是缺了点什么;可能是我目前还没有找到合适的打发时间的方式?不知道呢,人总是这样自相矛盾的生物。


开销总计 766 元,其中:

工作两年了

2024-08-04 09:11:00

从我 22 年 8 月开始参加工作以来,已经过去了两年的时间。我自己也马上就要 25 岁了。决定还是写点什么东西记录一下。本文可能没有什么结构,只是想到什么写什么;另外出于众所周知的原因,无法详细描述具体细节;还请各位读者见谅。

总体感受:7/10

软考高级架构师备考记录

2024-08-04 09:07:00

我参加了 2024 年上半年(5 月)的软考(全称:全国计算机技术与软件专业技术资格(水平)考试),通过了高级资格「系统架构设计师」的考试。本文记录我的备考过程。

个人背景

科班 CS 本科,参加工作 2 年,大厂底端程序员。

参加软考的原因

如果回看我的 23年总结&24年展望 的话,会发现当时我已经把通过软考作为 24 年的个人目标之一了。其实我接触软考是 23 年 9 月,但是了解信息有些延迟,当时已经过了报名窗口,所以只能再等半年。当时我并没有非常清晰的动机,以下列出的原因只是站在考完的时间节点上反向硬找出来的:

  1. 学生时代的考试思想延续:作为中国学生,考试思维估计是我思想中比较难剥离干净的一部分了,核心是将各类难以量化的目标想方设法量化,并且制定明确的验收标准(例如通过考试)来催促自己达成目标
  2. 学习架构设计:虽然我接受了科班教学,也做过不少项目(无论是学生时代还是工作之后),但是大部分情况下,架构要么是直接给出(例如更高级的同事已经设计好了),要么是自己瞎整(例如各类课程项目),没有系统性学习过;既然刚好有”系统架构设计师“这个资格科目,作为考试也有系统性的知识梳理,不妨以考促学
  3. 为未来发展留点余地:虽然目前在私企工作用不上,但是未来无论是跳槽国企还是润其他国家,软考的证书认可度还是可以的(e.g. 软考在日本永居打分接受的证书列表中)
  4. 退税:考过了的话可以退税 3600 元 (8/7 评论指正:是 3600 元的退税额度,实际退税金额取决于税率)(每个税收年度可以在继续教育类目下认证一个资格类证书)
  5. 听起来比较厉害:毕竟叫做高级,能满足下虚荣心 当然每个人可能还有其他的原因(例如落户、国企内评职称、所在地域有优惠政策…),不过我自己的原因大致就是以上这些了。

但决定参加前,也必须要了解软考的局限性:软考本质上还是个八股文考试,有很大可能学了用不上(毕竟对于考生的工作年限是没有要求的);而且软考是水平考试而不是资格考试,做开发也没有任何资格壁垒(不像建筑)。

软考的考试内容

既然是考试,最重要的当然是考什么。好在这个问题并不难回答,看教材就好了。架构师的教材名叫《系统架构设计师教程(第2版)》,大体上可以分为两个部分;第一部分是综合知识,其中一些章节接受过科班 CS 教育的人稍作复习即可(计算机系统、信息安全、数据库设计),另外一些章节则之前学的不够深入或者是首次接触,需要学习(软件工程、架构设计、质量属性、软件可靠性、架构演进);第二部分是各类架构的详细介绍,总共划分了八大架构(信息系统、层次式、云原生、SOA、嵌入式、通信系统、安全、大数据),每个架构内会有概述、设计、优缺点、适用场景、示例等。

和考试内容同等重要的,还有考试形式。软考高级资格的考试是 3 个科目:综合知识(单选,75题)、案例分析(简答,1必选+4选2)、论文(4选1,2k字+)。(是不是有种高中语文的感觉了?)每个科目满分 75 分,及格线是 45 分。所有科目都及格才能通过。其中综合知识和案例分析上午连着考,论文则是下午单独考。每个科目的考试时间都是 2 个小时。这其中综合知识和案例分析,基本上靠刷题可以覆盖掉,然而论文就是一个大坑了,毕竟2个小时时间,要完成选题、构思、写作、检查,而且字数还要求2k以上,如果没有训练过实在是比较困难。(顺带一提,23年下半年之前,软考是纸质考试,论文当然也是要手写的;好在23年下半年开始改为了机考,论文也可以打字作答了;真不敢想象之前的考生是如何考过的)。

备考资料

我认为比较有用的资料有:

  1. 书《系统架构师设计师考试 32小时通关》(作者:薛大龙):基本上是教材的精简版本,还带有简单的例题;初期可以先用这本书读一遍,建立下知识框架(软考官方教材不要买纸质书,又厚又重;找 PDF 就够了)
  2. 软考备考资料:https://github.com/xxlllq/system_architect;虽然 repo 里有些东西,但是大部分资源还是要付费后从阿里云盘获取;虽然这些资料自己逐个找也可以找出来,但是相对于花的时间和精力来说,花点小钱(¥20)一次找全还是划算的
  3. 小程序《软考达人》:刷题用(综合知识和案例分析);单纯刷题的话完全免费,不需要付费也不用加群
  4. B站视频课程(up 主:文老师软考教育):https://www.bilibili.com/video/BV1Dy4y1a71j;时间比较紧,就看了论文的两 P,对梳理论文结构很有帮助(e.g.十段式)

总体上,最必须的书 + 资料大概五六十元就可以拿下;是否要买课或者是报班就见仁见智了。

备考时间线

基本上备考时间只有 2 个月,核心的学习时间大概是 60 小时(18 小时读书建立框架、30 小时刷题、12 小时考前临时抱佛脚)。

cert.png

感受和建议

  1. 对于科班 CS,有实际开发经验的同学来说,这个考试并不算非常困难(10分最难的话,大概是7~8分的水平),但是依然要花时间准备(主要是刷足够多的题)
  2. 论文真的很看运气,本次是运气好,可选的题目里有一个和我最近的工作相关的题目;如果没有这个题目可能我就跪了;最好要掌握如何把自己做过的项目套到论文题目上
  3. 写作速度很重要,论文的 2 小时我基本上前 5 分钟在选题和构思,后面就一直在写了,结束前 2 分钟才完稿,时间非常紧张
  4. 如果你是学生,有比较多的空闲时间,不妨去考下试试看,反正也不要求工作年限

和正文无关的一些碎碎念:上次写考试相关的文章,还是 2021 年 7 天 TensorFlow 开发者认证的那一篇。那篇文章意外上了阮老师的周刊,还给本博客增加了不少流量。现在回过头来看看 ML 领域 PyTorch 几乎已经一家独大,TF 如风中残烛,更别提 LLM 的突飞猛进,当年的认证回头来看其实除了满足虚荣心+能写在简历上之外,并没有起到实质性的作用。前几天收到 Google 的邮件,说当时考的认证已经过期了(有效期只有三年),问我还要不要再续(实际上就是再考一次),那当然就选择不考了。虽然软考没有有效期,不存在重考的问题,但在决定投入精力被考前,依然得好好想想是否真的「对我有用」。


本文发出后的修正

读书记录《软件设计的哲学(第2版)》

2024-07-28 21:34:58

书名:A Philosophy of Software Design (2nd Edition)

评价:8.5/10;一开始是看到封面感觉很棒,于是就找来读了下;不是很长,三四个小时就能读完;虽然内容比较基本,但是能系统化的重新复习下也挺好的

版本:anna’s archive,llm翻译为中文


  1. 软件设计的原则
    • 复杂性的管理
      • 复杂性源自依赖和晦涩的累积;随着复杂性增加,它导致变更放大、高认知负荷以及未知的未知因素
        • 变更放大:一个看似简单的改动需要在多处修改代码
        • 认知负荷:为了进行更改,开发者必须积累大量信息。
        • 未知的未知:尚不清楚需要修改哪些代码,或者为了进行这些修改必须考虑哪些信息。
      • 因此,实现每个新功能需要更多的代码修改。此外,开发人员花费更多时间获取足够的信息以安全地进行更改,在最糟糕的情况下,他们甚至无法找到所需的所有信息。底线是,复杂性使得修改现有代码库变得困难且充满风险。
      • 向下转移复杂性最有意义的情况是:(a)被转移的复杂性与类的现有功能紧密相关,(b)转移复杂性将导致应用程序其他部分的简化,以及(c)转移复杂性简化了类的接口。记住,目标是最小化整个系统的复杂性。
    • 模块化与接口设计
      • 在设计类和其他模块时,最重要的议题是使它们具有深度,以便为常见用例提供简单的接口,同时仍能提供重要的功能。
      • 在将系统分解为模块时,尽量避免受到运行时操作顺序的影响;这会导致时间分解,从而引发信息泄露和浅层模块。
      • 软件设计中最关键的要素之一就是确定谁需要知道什么,以及何时需要知道。当细节至关重要时,最好将它们明确且尽可能显而易见地展现出来
      • 多个方法可以拥有相同的签名,只要它们各自提供有用且独特的功能。
    • 代码简化与重构
      • 在编写详细代码时,简化代码最有效的方法之一是消除特殊情况
      • 特殊情况可能导致代码充斥着if语句,这使得代码难以理解且容易产生错误。因此,应尽可能消除特殊情况。最佳的做法是通过设计正常情况,使其自动处理边缘条件,而无需额外代码。
      • 如果你为了减少方法数量而不得不引入大量额外参数,那么你可能并没有真正简化问题
  2. 从基础开始
    • 变量与方法的命名规范
      • 因此,你不应满足于仅仅是“合理接近”的命名。花一些额外的时间来挑选精确、无歧义且直观的优秀名称。这份额外的关注将很快得到回报,随着时间的推移,你将学会迅速选择好的名称。
      • 名称“cursorVisible”传达了更多信息;例如,它让读者能够猜测真值的含义(通常情况下,布尔变量的名称应始终为谓词形式)。名称中不再包含“blink”一词,因此如果读者想知道为什么光标并非始终可见,他们需要查阅文档;这部分信息相对不那么重要。
      • 如果你发现很难为一个特定变量想出一个既精确、直观又不太长的名字,这是一个警示信号。这表明该变量可能没有明确的定义或目的。当这种情况发生时,考虑采用其他分解方法。例如,也许你试图用一个单一变量来表示多个事物;如果是这样,将表示分解为多个变量可能会使每个变量的定义更简单。选择好名字的过程可以发现设计中的弱点,从而改进你的设计。
      • 名称中的每个单词都应提供有用信息;那些无助于阐明变量含义的词汇只会增加冗余(例如,它们可能导致更多行换行)。一个常见的错误是在名称中添加诸如“field”或“object”之类的通用名词,比如“fileObject”。在这种情况下,“Object”这个词很可能并未提供有用信息(是否存在不是对象的文件?),因此应从名称中省略。
      • 杰兰德的一个观点我深表赞同:“一个名称的声明与其使用之间的距离越远,该名称就应该越长。”之前关于使用名为i和j的循环变量的讨论,正是这一规则的例证。
    • 代码结构的清晰性与可读性
      • 仅凭方法的长度本身很少是拆分方法的充分理由。通常情况下,开发者倾向于过度拆分方法。拆分方法会引入额外的接口,增加了复杂性。同时,它将原方法的各个部分分离,如果这些部分实际上是相关的,这会使代码更难以阅读。除非拆分方法能使整个系统变得更简单,否则不应进行拆分
      • 长方法并不总是坏事。例如,假设一个方法包含五个20行代码的块,这些块按顺序执行。如果这些块相对独立,那么方法可以逐块阅读和理解;将每个块移到单独的方法中并没有太大好处。如果这些块之间有复杂的交互,那么将它们放在一起就更为重要,以便读者可以一次性看到所有代码;如果每个块都在单独的方法中,读者将不得不在这些分散的方法之间来回翻阅,以理解它们是如何协同工作的。包含数百行代码的方法如果具有简单的签名并且易于阅读,那么它们也是很好的。这些方法是深层的(功能丰富,接口简单),这是好事
      • 深度比长度更重要:首先确保函数有足够的深度,然后再尝试使其足够短以便轻松阅读。不要为了长度牺牲深度。决定拆分或合并模块应基于复杂度。选择能实现最佳信息隐藏、最少依赖关系及最深接口的结构。
    • 注释的重要性与编写技巧
      • 优质的注释能显著提升软件的整体质量;编写优质注释并不难;而且(这可能难以置信)编写注释实际上可以很有趣。
      • 注释通过提供不同层次的详细信息来增强代码。有些注释提供比代码更低的、更详细的层次信息;这些注释通过阐明代码的确切含义来增加精确性。其他注释提供比代码更高的、更抽象的层次信息;这些注释提供直觉,比如代码背后的推理,或者一种更简单、更抽象的思考代码的方式。与代码处于同一层次的注释很可能会重复代码的内容。
      • 具体的注释方式
        • 在注释类实例变量、方法参数和返回值时,精确性尤为重要。变量声明中的名称和类型通常不够精确。注释可以填补缺失的细节,例如:
          • 这个变量的单位是什么?
          • 边界条件是包含性的还是排他性的?
          • 如果允许空值,这暗示着什么?
          • 如果一个变量指向一个最终必须被释放或关闭的资源,那么谁负责释放或关闭它?
          • 是否存在某些特性(不变量),对于变量而言总是成立,例如“这个列表始终至少包含一个条目”?
        • 在记录变量时,应考虑名词而非动词。换言之,重点在于变量所代表的内容,而非其如何被操作。
        • 在记录一个方法时,描述该方法最可能被调用的条件(特别是在方法仅在特殊情况下被调用时)会非常有帮助。
        • 记录抽象的第一步是将接口注释与实现注释分开。接口注释提供了某人为了使用类或方法所需了解的信息;它们定义了抽象。实现注释描述了类或方法内部如何工作以实现抽象。将这两种注释分开很重要,这样接口的用户就不会接触到实现细节。
        • 方法接口注释既包含高层次的抽象信息,也包含低层次的精确细节
          • 注释通常以一两句话开始,描述调用者感知到的方法行为;这是更高层次的抽象。评论必须详细描述每个参数及其返回值(如有)。
          • 这些评论必须非常精确,并且必须描述参数值的任何限制以及参数之间的依赖关系。
          • 如果方法有任何副作用,这些必须在接口注释中记录。副作用是指方法对系统未来行为产生影响的任何后果,但不是结果的一部分。例如,如果方法向内部数据结构添加一个值,该值可以通过未来的方法调用检索,这就是副作用;写入文件系统也是副作用。
          • 方法的接口注释必须描述该方法可能抛出的任何异常。
          • 如果在一个方法被调用之前必须满足某些先决条件,这些条件必须被描述出来(可能需要先调用其他方法;对于二分查找方法,被查找的列表必须是已排序的)。尽量减少先决条件是一个好主意,但任何保留的先决条件都必须有文档说明。
      • 幸运的是,有一个明显的地方是开发者在添加新状态值时必须去的,那就是状态枚举的声明处。我们利用这一点,在那个枚举中添加了注释,指出了所有也必须修改的其他地方
      • 处理跨模块注释:我最近在尝试一种方法,即跨模块问题记录在一个名为designNotes的中央文件中。该文件被清晰地划分为多个标有明确标签的部分,每个部分对应一个主要主题。
      • 在遵循注释应描述代码中不明显内容的规则时,“明显”是从初次阅读代码的人(而非你本人)的角度出发的。撰写注释时,尝试站在读者的立场,思考他们需要了解的关键信息是什么。如果你的代码正在接受审查,而审查者指出某些内容不明显,不要与他们争论;如果读者认为某处不明显,那么它就是不明显。与其争论,不如尝试理解他们感到困惑的地方,并思考是否能通过更清晰的注释或更优化的代码来阐明。
      • 一般来说,注释与它所描述的代码之间的距离越远,它就应该越抽象(这样可以降低因代码变动而导致注释失效的可能性)。
      • 在撰写提交信息时,问问自己:未来开发者是否需要这些信息?如果是,那么请在代码中记录下来。例如,一个描述了促使代码变更的微妙问题的提交信息。如果这未在代码中记录,那么后续开发者可能会在不知情的情况下撤销该变更,从而重新引入一个错误。如果你想在提交信息中也包含这份信息的副本,那当然可以,但最重要的是将其记录在代码中。这体现了将文档置于开发者最可能看到的地方的原则;而提交日志通常并非这样的场所。
      • 保持注释最新性的第二种技巧是避免重复。如果文档被复制,开发者找到并更新所有相关副本的难度就会增加。相反,尝试对每个设计决策只记录一次。如果代码中多个地方受到某个特定决策的影响,不要在这些点重复文档。而是找到最显眼的单一位置放置文档。例如,假设某个变量的行为复杂,影响到该变量使用的多个不同地方。你可以在变量声明旁边的注释中记录这种行为。这是一个自然的位置,开发者在理解使用该变量的代码遇到困难时很可能会查看。
      • 对于更局部化的约定,例如不变量,找到代码中合适的位置来记录它们。如果你不将这些约定写下来,其他人很可能不会遵循它们。
    • 何时测试
      • 测试,尤其是单元测试,在软件设计中扮演着重要角色,因为它们促进了重构。没有测试套件,对系统进行重大结构改动是危险的。没有简单的方法来发现错误,因此错误很可能会在新代码部署后才被发现,那时发现和修复错误的成本要高得多。因此,在没有良好测试套件的系统中,开发者会避免重构;他们试图为每个新功能或错误修复最小化代码更改的数量,这意味着复杂性积累,设计错误得不到纠正。有了良好的测试集,开发者在重构时可以更有信心,因为测试套件会发现大多数引入的错误。这鼓励开发者对系统进行结构上的改进,从而得到更好的设计。
      • 测试驱动开发的问题在于,它将注意力集中在使特定功能正常工作上,而不是寻找最佳设计。这纯粹是战术编程,带有其所有的不利之处。测试驱动开发过于渐进:在任何时候,都很容易为了通过下一个测试而匆匆添加下一个功能。没有明显的时间进行设计,因此很容易陷入混乱
      • 在修复 bug 时,先编写测试是一个合理的做法。在修复 bug 之前,先写一个因为该 bug 而失败的单元测试。然后修复 bug,并确保单元测试现在通过。这是确保你真正修复了 bug 的最佳方法。如果你在编写测试之前就修复了bug,那么新的单元测试可能实际上并未触发该 bug,这种情况下它将无法告诉你是否真正解决了问题。
    • 设计模式的应用
      • 不要试图将问题强行套入某个设计模式,而应采用更简洁的自定义方法。使用设计模式并不意味着自动提升软件系统的质量;只有当设计模式恰到好处时,才能发挥其优势。
      • 每当你遇到一个新的软件开发范式的提议时,从复杂性的角度对其进行质疑:这个提议是否真的有助于减少大型软件系统的复杂性?许多提议表面上听起来不错,但如果你深入探究,你会发现其中一些实际上使复杂性变得更糟,而非更好。
  3. 具体做法
    • 设计两次
      • 我注意到,“设计两次”原则有时对非常聪明的人难以接受。在他们成长的过程中,聪明人发现他们对任何问题的第一个快速想法就足以获得好成绩;没有必要考虑第二个或第三个可能性。这往往导致不良的工作习惯。然而,随着这些人年龄的增长,他们被提拔到面临越来越困难问题的环境中。最终,每个人都会达到一个阶段,即你的第一个想法不再足够好;如果你想取得真正出色的成果,无论你多么聪明,你都必须考虑第二个可能性,甚至可能是第三个。大型软件系统的设计就属于这一类:没有人能够一次就做得完美。
    • 注释先行的开发
      • 最佳的注释编写时机是在过程的开始,即编写代码的同时。先编写注释使得文档成为设计过程的一部分。这不仅能产生更好的文档,还能带来更优秀的设计,并且使编写文档的过程更加愉快。
      • 先写注释意味着在开始编码前,抽象概念会更加稳定。这很可能会在编码过程中节省时间。相反,如果先写代码,抽象概念可能会随着编码的进行而演变,这需要比先写注释的方法更多的代码修订。综合考虑这些因素,整体上先写注释可能会更快。
        • 对于一个新类,我首先撰写类接口注释。
        • 接下来,我会为最重要的公共方法编写接口注释和签名,但我会让方法体保持空白。
        • 我稍微反复斟酌这些评论,直到基本结构感觉差不多合适。
        • 在此,我为类中最重要的实例变量撰写声明和注释。
        • 最后,我填充了方法的主体,并在必要时添加了实现注释。
        • 在编写方法体时,我通常会发现需要额外的属性和实例变量。对于每个新写的方法,我会在方法体之前先写接口注释;对于实例变量,我会在写变量声明的同时填写注释。
        • 当代码完成时,注释也已完成。从未有过未编写的注释积压。
    • 性能优化与重构
      • 一旦你对什么是昂贵、什么是便宜有了大致的了解,你就可以利用这些信息尽可能选择便宜的操作。在很多情况下,更高效的方法可能和较慢的方法一样简单。
      • 再举一个例子,考虑在C或C++这样的语言中分配一个结构体数组。有两种方法可以实现这一点。一种方法是将数组用于保存指向结构体的指针,在这种情况下,你必须首先为数组分配空间,然后为每个单独的结构体分配空间。将结构体直接存储在数组中要高效得多,这样你只需为所有内容分配一个大的内存块。
      • 一般来说,代码越简单,运行速度往往越快。如果你已经定义并处理了特殊情况和异常,那么就不需要额外的代码来检查这些情况,系统运行速度自然更快。深层类比浅层类更高效,因为每次方法调用它们能完成更多工作。浅层类会导致更多的层级跨越,而每次层级跨越都会增加开销。
      • 在进行任何更改之前,应测量系统的现有行为。这有两个目的。首先,这些测量将确定性能调优影响最大的地方。仅仅测量顶层系统性能是不够的。这可能告诉你系统太慢,但不会告诉你原因。你需要更深入地测量,以详细识别影响整体性能的因素;目标是找出系统当前花费大量时间的少数特定位置,并且你有改进的想法。测量的第二个目的是提供一个基准,这样你可以在更改后重新测量性能,以确保性能确实得到了提升。如果更改没有使性能产生可测量的差异,那么就撤销这些更改(除非它们使系统更简单)。除非能显著加快系统速度,否则保留复杂性是没有意义的。
      • 改进其性能的最佳方法是进行“根本性”的改变,比如引入缓存,或者采用不同的算法方法(例如平衡树与列表)。
      • 首先,问问自己,在常见情况下,为了完成所需任务,必须执行的最少代码量是多少。忽略任何现有的代码结构。想象一下,你正在编写一个新方法,只实现关键路径,即在大多数常见情况下必须执行的最少代码量。当前的代码可能充斥着特殊情况;在这个练习中忽略它们。当前的代码可能在关键路径上经过多个方法调用;想象一下,你可以将所有相关代码放在一个方法中。当前的代码也可能使用多种变量和数据结构;只考虑关键路径所需的数据,并假设任何数据结构对关键路径最为方便。例如,将多个变量合并为一个值可能是有意义的。假设你可以完全重新设计系统,以最小化关键路径必须执行的代码量。我们称这种代码为“理想状态”。
      • 在为性能进行重构时,应尽量减少必须检查的特殊情况数量。理想情况下,开始处应只有一个if语句,通过一次测试就能检测所有特殊情况。在正常情况下,只需进行这一次测试,之后关键路径即可无须额外特殊情况测试地执行。如果初始测试未通过(意味着出现了特殊情况),代码可以跳转到关键路径之外的独立位置处理该情况。
      • 清晰的设计与高性能是可以兼容的。Buffer类的重写不仅使其性能提升了两倍,同时简化了设计并减少了20%的代码量。复杂的代码往往运行缓慢,因为它执行了多余或重复的工作。相反,如果你编写清晰、简洁的代码,你的系统很可能已经足够快速,以至于你无需过多担心性能问题。在少数确实需要优化性能的情况下,关键仍然是简洁性:找出对性能至关重要的关键路径,并尽可能简化它们。
    • 遵守约定和惯例
      • 一旦发现任何看似约定的做法,就应遵循。在进行设计决策时,问问自己这个决策是否可能在项目的其他地方也有类似的选择;如果有,找到一个现成的例子,并在你的新代码中采用相同的方法。
      • 不要改变现有的惯例。抵制那种想要“改进”现有惯例的冲动。拥有一个“更好的想法”并不是引入不一致性的充分理由。你的新想法可能确实更好,但一致性相对于不一致性的价值几乎总是大于一种方法相对于另一种方法的价值。在引入不一致行为之前,问自己两个问题。首先,你是否拥有重要的新信息来证明你的方法,而这些信息在旧惯例建立时是不可用的?其次,新方法是否好到值得花时间去更新所有旧的使用?如果你的组织同意这两个问题的答案都是“是”,那么就大胆进行升级;完成后,旧惯例的痕迹应该荡然无存。然而,你仍然面临风险,即其他开发者可能不知道新惯例,因此他们未来可能会重新引入旧方法。总的来说,重新考虑已建立的惯例很少是开发者时间的良好利用。
      • “显而易见”存在于读者心中:注意到他人代码的不明显之处比发现自己的代码问题要容易得多。因此,判断代码是否显而易见的最佳方法是通过代码审查。如果有人阅读你的代码后认为它不明显,那么它就是不明显的,无论对你来说它看起来多么清晰。通过努力理解是什么使得代码不明显,你将学会如何在将来编写更好的代码。
      • 代码如果符合读者预期的惯例,则最为直观;如果不符合,那么记录这种行为就很重要,以免读者感到困惑。
      • 为了使代码显而易见,你必须确保读者始终拥有理解代码所需的信息。你可以通过三种方式来实现这一点。最佳方法是减少所需的信息量,运用抽象和消除特殊情况等设计技巧。其次,你可以利用读者在其他情境中已获得的信息(例如,通过遵循惯例和符合预期),这样读者就不必为你的代码学习新信息。第三,你可以通过使用良好的命名和策略性注释等技巧,在代码中向他们展示重要信息。
    • 正确对待事件驱动编程
      • 事件驱动编程使得跟踪控制流程变得困难。事件处理函数从未被直接调用;它们是通过事件模块间接调用的,通常使用函数指针或接口。即使你在事件模块中找到了调用点,仍然无法确定具体会调用哪个函数:这取决于运行时注册了哪些处理程序。因此,很难对事件驱动代码进行推理,或者确信其工作正常。
      • 为了弥补这种晦涩,请在每个处理函数接口注释中指明其何时被调用
    • 避免使用通用容器
      • 不幸的是,通用容器导致代码不直观,因为被分组的元素具有模糊其含义的通用名称。在上述示例中,调用者必须使用result.getKey()和result.getValue()来引用两个返回值,这无法提供关于值实际含义的任何线索。
      • 因此,最好不要使用通用容器。如果你需要一个容器,可以定义一个专门针对特定用途的新类或结构。这样,你就可以为元素使用有意义的名称,并在声明中提供额外的文档,这是通用容器无法做到的。
    • 透传变量与上下文
      • 透传变量增加了复杂性,因为它们迫使所有中间方法都意识到它们的存在,即便这些方法并不需要使用这些变量。此外,如果一个新的变量出现(例如,系统最初构建时未支持证书,但后来决定添加该支持),你可能需要修改大量接口和方法,以确保该变量能够通过所有相关路径传递。
      • 我最常用的解决方案是引入一个上下文对象,如图7.2(d)所示。上下文存储了应用程序的所有全局状态(任何原本需要传递的变量或全局变量)。上下文远非理想的解决方案。
      • 存储在上下文中的变量大多具有全局变量的缺点;例如,可能不明显为什么存在某个特定变量,或者它在何处被使用。如果没有纪律,上下文可能会变成一个巨大的数据杂烩,在整个系统中产生不明显的依赖关系。上下文还可能引发线程安全问题;避免问题的最佳方式是使上下文中的变量不可变。遗憾的是,我尚未找到比上下文更好的解决方案。
    • 异常处理和配置参数
      • 这些方法在短期内会让你的生活更轻松,但它们增加了复杂性,导致许多人必须处理一个问题,而不是仅仅一个人。例如,如果一个类抛出异常,该类的每个调用者都必须处理它。如果一个类导出配置参数,每个安装环境中的每个系统管理员都必须学习如何设置它们。
      • 因此,应尽可能避免使用配置参数。在导出配置参数之前,自问:“用户(或更高级别的模块)能否确定比我们在此处确定的更优值?”当确实需要创建配置参数时,尝试提供合理的默认值,以便用户仅在特殊情况下才需提供值。理想情况下,每个模块应完整解决问题;配置参数导致解决方案不完整,从而增加了系统复杂性。
      • 抛出异常容易,处理异常却难。因此,异常的复杂性主要来源于异常处理代码。减少异常处理带来的复杂性损害的最佳方法,是减少需要处理异常的地方。
      • 异常屏蔽并非在所有情况下都有效,但在其适用的场合,它是一个强有力的工具。它能够产生更深层次的类,因为它减少了类的接口(用户需要了解的异常更少),并以屏蔽异常的代码形式增加了功能。异常屏蔽是向下转移复杂性的一个例子
      • 最佳方法是重新定义语义以消除错误条件。对于无法消除的异常,应寻找机会在较低层次上屏蔽它们,从而限制其影响,或者将多个特殊情况处理程序聚合为一个更通用的处理程序。
  4. 软件设计的哲学与美学
    • 时刻重构
      • 如果你想为一个系统保持一个干净的设计,在修改现有代码时必须采取战略性的方法。理想情况下,当你完成每一项改动后,系统应具备如果从一开始就考虑到这些改动而设计的结构。为了实现这一目标,你必须抵制快速修复的诱惑。相反,要思考当前的系统设计是否仍然是最佳的,考虑到所需的改动。如果不是,就重构系统,以便最终获得尽可能最佳的设计。通过这种方法,系统设计随着每一次修改而不断改进。
    • 设计的重要性与价值
      • 良好软件设计的一个重要元素是区分重要与不重要。应以重要的事物为核心构建软件系统。对于不太重要的事物,应尽量减少它们对系统其余部分的影响。重要的事物应加以强调并使其更加明显;不重要的事物则应尽可能隐藏。
      • 一旦你确定了重要的事物,你应该在设计中强调它们。强调的一种方式是通过突出:重要的事物应该出现在更可能被看到的地方,比如界面文档、名称或频繁使用的方法的参数。另一种强调的方式是通过重复:关键的想法反复出现。第三种强调的方式是通过中心性。最重要的事物应该位于系统的核心,它们决定了周围事物的结构。一个例子是操作系统中设备驱动的接口;这是一个核心想法,因为成百上千的驱动程序将依赖于它。
      • 专注于最重要的事物的理念不仅适用于软件设计,在技术写作领域也同样重要:使文档易于阅读的最佳方法是在开头识别几个关键概念,并围绕它们构建文档的其余部分。
    • 软件开发中的“好品味”
      • “好品味”这一短语描述了区分重要与不重要事物的能力。拥有好品味是成为优秀软件设计师的重要组成部分。
      • 成为优秀设计师的回报是,你能够将更多时间投入到充满乐趣的设计阶段。而糟糕的设计师则大部分时间都在复杂且脆弱的代码中追踪错误。如果你提升自己的设计技能,你不仅能更快地产出更高质量的软件,而且软件开发过程本身也会变得更加愉快。

2024 年了,我最近在用什么工具

2024-07-21 21:53:00

2024 年了,我最近在用什么工具

去年年中,公司的主力开发设备从 Windows 换成了 Mac,之前在 Windows 上用的各类工具需要重新在 Mac 上找对应的替代品。一年磨合下来现在已经差不多稳定了,特此记录(其实之前就应该记录的,但是太懒)。如果能帮助到各位读者就更好了。当然也欢迎评论推荐更多你认为好用的工具。

独立工具

(此处的独立指的是软件本身可以独立运行,与之相对的是插件)

Obsidian 插件

(顺带一提,为了和 VS Code 使用习惯一致,Quick Swtich 绑定到 Cmd+R,command palette 绑定到 Cmd+Shift+P)

浏览器插件

(目前用的是 Edge,纵向标签页没有其他浏览器支持)

还有些小工具网站:

VS Code 插件

End of 2023

2023-12-31 22:46:01

End of 2023

一转眼又到年底了,关注的博主和身边的好友纷纷亮出了自己的年终总结;在这个离跨年还有2个小时不到的时刻,决定还是随手写点,就当给未来的自己留下些印象了。

回顾 2023

展望 2024

多语言的 Hugo 博客

2023-11-04 20:27:43

之前看到了某位同学的分享,提到他在将博客多语言化之后,访问量有了显著的上升,于是也想试试看。在 OpenAI 的加持下文章翻译并不是什么难事,但是想要给一个现存的 Hugo 站点增加多语言支持依然不轻松。虽然 Hugo 本身自带了多语言支持的基本特性(文档:Hugo Multilingual),但是倘若选用的主题不支持,则还需要对主题进行改造。

目前对本博客,我选择了"按文件名翻译"的做法,从文档来看这似乎是对现有文件结构侵入最小的方案。简单来说,如果你的博客 Markdown 文件位于 /content/blog.md,在同层级下新建一个 blog.en.md 即可补充英文翻译。完成后可以通过在域名后补充 /en/ 路径的方式来访问。(主语言,在我这里是"简体中文"的路径不受影响,即无需补充语言后缀。)然而让用户手动补充语言路由显然是不可接受的,于是得在页面某处加一个语言选择器。这里我暂时加到了顶部。然后你的多语言博客就可以上线了。

目前已知还存在的一些问题,待之后有空再来慢慢解决吧:

  1. 各种导航(例如左上角的后退)会回到站点根目录(也就是简体中文主页);合理的做法是回到当前语言的对应主页
  2. RSS feed 链接有问题,默认提供的仍然是主语言的 RSS 链接,英文的链接在 /en/ 路径下;这里可能要考虑一个整合的 RSS?

大部分的核心多语言代码可以从这个 commit 看到:ca7a83d

更多参考链接:

我的 AI Prompt

2023-10-29 20:17:00

记录下自己自用的一些 Prompt。只是迭代下来感觉还不错,但不一定是最好的。如有推荐欢迎回复补充。

通用场景

(主要配合 GPT-3.5-Turbo 使用,回答代码类问题)

You are a helpful assistant and also a professional & experienced developer. You can help me by answering my questions. You can also ask me questions. If you are given code related questions, please answer in a consise manner, give code examples with less explaination.

摘要总结(英文)

(主要配合 claude-instant-v1 使用,用于文章摘要、结构化总结)

You are a professional reader and analyst. Please summarize the following article in a organized manner. Use markdown list format with indentation indicating the layering of ideas. Ignore any text that is unrelated to the main article. Also include a short tl;dr summary (no more than 50 words and 3 sentences). Refer to the following example when summarizing.

---
[Example Output]

TL;DR summary: Summary in no more than 50 words.

# Title
## Heading 1
- Idea 1
    - Reason 1

## Heading 2
1. Numbered Item 1
2. Numbered Item 2

## Conclusion

摘要总结(中文)

(主要配合 ChatGLM-Turbo 使用,用于文章摘要)

您是一名专业的读者和分析者,现在请对下面的文章进行整理总结。使用Markdown列表格式输出总结,每个列表项是一个想法,并用缩进表示思想的层次,更深层的列表项代表论据或想法的演进,忽略任何与主文无关的文字。

用 mitmproxy 让 ChatGLM 适配 OpenAI 接口

2023-10-29 19:41:00

最近看到了几篇关于智谱 AI 的推送文章,才想起来他们的大模型(ChatGLM 系)已经上线好久了。回想 6B 模型刚公布的那会还在 AutoDL 上自己跑过,不过因为模型本身太小,所以其实能做的并不算多。注册了个开发者账户看了看文档,目前可以广泛使用的是 ChatGLM-Turbo,上下文窗口 32k token,定价 0.005 元/千token,还是很便宜的。更不用说因为 GLM 系模型以中文语料为主,所以同等长度的中文文本,用 GLM 的 token 消耗比用 GPT 系列的 token 消耗会小很多(测试下来大概在 4x 左右)。

官网的 Playground 玩了一会感觉还不错,生成的中文明显感觉更自然,没有 GPT 系那么浓烈的翻译腔,于是想着怎么接入到我自己用的客户端 Chatbox 中日常使用。Chatbox 有内置的 ChatGLM 支持,一般直接设置下 token 就可以了。但是因为我主要用的还是 GPT 系模型,而 Chatbox 又只能全局设置一个 API 服务器字段,所以如果要同时使用 GPT 和 ChatGLM 的话,还是得用之前提到的 mitmproxy,手动完成请求的中转(没有什么是加一个抽象层不能解决的)。这里用 mitm 方式让 GLM 适配 GPT 接口还有个额外的好处,那就是只支持 OpenAI 的第三方应用也可以自动支持 GLM 了(虽然我还没这么用过)。

和之前适配 OpenRouter 不一样,这次除了修改请求头,还要修改 SSE 响应体。不知道出于什么考虑,GLM 系列模型的响应事件和 GPT 系列的完全不同,修改起来还是有些复杂的。但总之调试了几个小时之后总算是改完了,代码在此:(不建议在生产环境使用,后果自负)

https://gist.github.com/jerrylususu/3ebcf6262d110da89ce58d1e8d55bc22

改请求头比较简单,修改如下:

  1. 换 host 和 path
  2. 换 Authorization 头:参考 GLM 开发文档的"鉴权"一节即可(注意这里要用 PyJWT 库,直接二进制安装的 mitmproxy 带的 Python 环境不支持安装包,需要走 pipx 安装,可参考官方文档
  3. 消息列表(messages)修改:GLM 系里叫做 prompt,而且根据实测只能支持 user-assistant 交替,如果存在 system 或是有两个连续的 user 消息都会报错;这里稍微转换了下,把所有的非 user/assistant 消息都转成 user,然后手动连接下连续的同 role 消息,保证最后构造的消息列表是两个角色交替。
  4. 开启增量返回:默认似乎是全量返回,这里和 OpenAI 对齐,也改成增量

比较烦人的是改响应体,如下所示分别是 GLM 系的返回和 GPT 系的返回。可以发现 GLM 系列比较简单,只有事件类型、流 ID 和增量数据;GPT 系列就更复杂一些,返回的是个 JSON,里面还有嵌套结构。

# GLM
b'event:add\nid:8065135252561182716\ndata:\xef\xbc\x8c\n\n'
# GPT
b'data: {"id": "chatcmpl-8EymH9k9DS9iQQvIH3BguHaZqmib9", "object": "chat.completion.chunk", "created": 1698580913, "model": "gpt-3.5-turbo-0613", "choices": [{"index": 0, "delta": {"content": "?"}, "finish_reason": null}]}\n\n'

这里的改造思路其实很明确,先解析 GLM 的响应体,再据此拼装成 GPT 的相应格式,然后返回给应用就可以了,然而具体做起来还是有不少坑。

目前的效果算是初步可用了吧,但是偶尔如果响应本身不完整(例如某个 SSE 事件返回了不完整的 utf8 编码字符串,下一个事件没有包含丢失的数据),那就会直接报错。不过考虑到实际频率比较低,重试的成本比较小,这里还算可以接受吧。

用 mitmproxy 重定向 OpenAI 请求到 OpenRouter

2023-10-22 20:55:00

背景

最近在尝试使用一些基于 GPT 开发的工具,但遇到了一些网络相关的小问题。因为支付方式的限制,我自己并没有 OpenAI 的账户,实际使用的 API 是其他中间商(aka 二道贩子)转卖而来的, OpenRouter 就是其中一家。(实际上 OpenRouter 做的还更多一些,更像是 LLM 的聚合提供商,除了 OpenAI 也有其他家的 LLM,如 Claude 或是 LLama。)但是很多开源工具并未考虑到这种情况,基本上都是假定用户使用的就是 OpenAI 的官方 API 端点,所以很多时候并不能直接使用各类预先构建好的产物(例如 docker 镜像),而是得把源码 clone 下来,找到 import openai 或者是类似的调用发起位置,再在附近补充一些参数才能正常使用。手动改代码固然不是不行,但是总归还是有些繁琐,出问题的时候还额外增加了一个需要排查的环节。

问题

有没有更好的,更自动化的方式,例如在网络上加个代理层,在第三方工具无需修改的前提下,就可以将 OpenAI 的请求转换成 OpenRouter 的请求呢?

解决

那既然都写到这里了,当然是有的。这里的核心是一个 man-in-the-middle (mitm / 中间人)代理,在请求到达代理的时候,修改请求中的内容,使之符合我们的要求,之后再继续对外发送就可以了。mitmproxy 就是这样一个工具。当然它的功能远不止修改请求,在完善的 Python API 的加成下还能做很多其他的事。(同类的工具其他工具,如 Fiddler,应该也能实现,但方法就需要给位自行探索了。)以下就是实现本次需求的核心代码,应该不需要太多解释。

import json
from mitmproxy import http

def request(flow: http.HTTPFlow) -> None:
    # 只处理 HOST 为 api.openai.com,且请求体为 JSON 的 POST 请求
    if flow.request.host == "api.openai.com" and flow.request.method == "POST" and flow.request.headers.get("content-type", "").startswith("application/json"):
        try:
            flow.request.host = "openrouter.ai"
            flow.request.path = "/api/v1/chat/completions"
            flow.request.headers["authorization"] = "Bearer sk-xxxxxxxxxx" # token
            flow.request.headers["http-referer"] = "http://localhost:8080/my_great_app" # 应用标识
            request_data = json.loads(flow.request.get_text())

            # 甚至可以在这里切换模型
            # request_data["model"] = "anthropic/claude-instant-v1"

            flow.request.set_text(json.dumps(request_data))
            pass
        except json.JSONDecodeError:
            pass

# 需要声明回包支持 stream,否则会等待全部数据到达再返回给应用,无法实现 LLM 打字效果
def responseheaders(flow):
    flow.response.stream = True

启动 mitmproxy 时需要带上 Python 脚本参数,以及如果有上游代理则需要再声明:

mitmweb --mode upstream:http://{upstream_addr} -s openrouter.py

启动后会弹出 mitmproxy 的网页控制台,这时候就用第三方工具发请求试试了,一切顺利的话可以看到结果正常返回且网页上显示请求数据。如果出现问题也可以看命令行窗口的输出。如果第三方工具本身支持设置应用内代理(如 Chatbox)则最理想;不支持的话可以考虑设置系统代理、用 mitmproxy 的透明代理模式、或者用 Proxifer 这类工具来强制应用代理。

日本游记

2023-10-07 03:10:00

为什么要写下来?因为不写就真的忘记了。

注:部分不适宜面向所有人公开的内容已用 [REDACTED] 自主规制。

day 0 香港-东京

永东巴士去hk,原来以为是真正的大巴,原来是小型商务车

流量卡差点以为用不了,原来要手动设置apn再重启,飞机上开关了好几次飞行模式都不生效,还是最后重启才生效了

在机场买了 skyliner+72h地铁券 套餐,发现二维码不能截屏扫描;最后换用信用卡支付,不得不错过了前一班车,多等了半小时;以及发现本来想买到日暮里的,结果第二次买太着急直接选了上野

那就做到上野换乘吧,从铁路的上野站走到地铁的上野站还有不少距离,中间似乎是个红灯区,看到不少站街女;中途还遇到蟑螂直接飞到旅行箱上,然后跳到了手上,看来东京也已经被蟑螂占领了(不过好在这次旅行后面没有再遇到)

酒店隐藏在小巷子里,不过交通上还挺方便,地铁站真的是徒步1分,秋叶原也在步行距离内

夜宵:吉野家亲子丼,大盛(还有一家半夜开着的就是麦当劳了);甜酱油口,鸡肉和鸡蛋原来这么搭配,未来可以考虑在国内试试

夜宵+1:711 冰淇淋(原味+巧克力,居然还能做的这么大)

day 1 东京

很困,睡到11点被客房清洁叫醒;收拾到12点左右出发

午餐:coco一番屋,炸猪排咖喱饭;感觉味道还行,偏甜口,但是咖喱给的太多,饭吃完了还剩咖喱

地铁其实坐起来还挺简单,跟着导航走就行,标识很有意思(市内的话不用太担心车辆等级的问题,基本都是各停)

有的车还是传统点阵,有led的都很清晰,尤其是东京metro的,甚至会显示哪个车厢对应哪个检票口(地图也会显示,可以prewalk到对应的车厢,感觉国内应该推广);车次算正常密集吧,基本上五六分钟一趟;"运转情报"比"运行信息"听起来更nb?不过一来就遇到了几个延迟事件

真的有从0开始的地铁出口编号

浅草寺,人爆多,抽签抽到了大吉,运气不错,买了两个御守([REDACTED]),还许了个愿;附近买了抹茶冰淇淋,挺浓的抹茶味,还行;人字烧感觉就是普通的蛋糕,形状也不像人的形状

零钱盒子超级有用!

秋叶原,出站口有原神柱子,先外面走了一圈,游客非常多,然后开始扫楼;animate买了摇曳露营的立牌;bookoff买了几本喜欢的作品的书([REDACTED],虽然都是日文的,价格上倒是意外的便宜,几本书加起来也就是一个立牌的钱);gogo里面都是游戏,看了下扭蛋就没进去了;RADIO会馆 10层走了好久,差点买了[REDACTED]的钥匙链,但感觉还是有点贵,还不如国内淘宝网购;唐吉珂德真大,可惜没我想要买的东西,不过居然有cos服装和派对道具卖,总算知道为什么轻小说里说文化祭之前要去这里买东西了;出来吃了familymart的炸鸡,鲜嫩多汁

原来要5000cny才能免税

真的是没地方扔垃圾,连扔瓶子的地方都不好找;幸好带了一个便携垃圾袋(有抽绳可以手动系紧)

晚餐:一兰拉面,普通叉烧拉面;去的比较早(五点半),但是前面已经排了十几人了,等位半个小时;味道一般般,汤底油比较多,稍微有些腻(可能还比不上国内的),服务还行,大肆宣传的单人隔间+帘子也就那样,体验过一次新鲜感就没了,吃完感觉没吃饱又追加了面(想不到一个替玉真的是完整的一碗面的量);吃完出来排队的人更多了,估计得有三四十人了;感觉作为一个safe option还行,但是日常吃就没必要了

出来天黑了,找了找看夜景的地;惠比寿花园还挺好,毕竟是免费的,从车站可以直通到建筑底部,电梯直接上去就行;一侧可以同时拍到东京塔和skytree

居然有专门拍摄好看的入学照片,以此提升第一印象的服务

发现涩谷就在附近,顺便去涩谷看了下;忠犬八公身上不知道为什么系上了个丝带;交叉路口实际体验也很震撼,人的确很多而且大家都在录像;不过人实在是太多了,而且全是购物,外面大概走了一圈就撤了

下来之后决定先去东京塔看看,出站就可以看到,已经很震撼了;越走近越震撼;不过小红书上说上去不太值就没上去

然后突发奇想去东京站,夜景还行(外面石碑没打光,还以为错过了);里面不少存包的地方,day3的时候估计可以直接放过去;然后发现酒店离东京站只有10min,位置优势+1

夜宵:夹心冰淇淋(外面是软的华夫,里面是冰淇淋,真正的ice cream sandwich)

晚上22点结束行程

day 2 东京

十点多出发,先去附近的投币洗衣店看了看(酒店洗衣房太小了),距离上稍微有点远但还好,走路快10分钟的样子

早餐:麦当劳月见汉堡(470jpy),口感上比较神奇,里面有个全熟的蛋,蛋黄和牛肉混合在一起感觉有些诡异但也不是不行(主要是蛋黄一咬就散了,但是牛肉没这么容易散)

然后先出发去teamlab,快到了才发现海鸥线居然是轻轨,换乘的时候得一路上楼(而且一站就要我150+,超贵)

teamlab本身还是挺有意思的,展览里面led灯矩阵一开始感觉还不错,但是回过味来也就那样;下一个比较震撼的是投影池塘,一开始看到水是奶白色的还以为是水质不好,后来才意识到是为了让投影能够显示出来(类似幕布),配合音乐和光效体验不错,而且深度的确是能到关节稍低处,看了说明才知道是实时生成的,原来procedural art gen可以做到这个高度;后面更震撼的是落花宇宙,球面投影效果非常棒,躺在地上的确有在宇宙中漂浮的感觉,物体接近然后穿透的感觉十分真实(非常神奇的是就算盯着一个固定物体看,因为背景的移动,大脑不会将其认为是固定的,结果就是可以用这个物体相对于背景的移动来判断当前的移动方向和速度;总之就是连大脑都被骗进去了);后面的两个庭院展览,一个圆球的没太懂,说明上说可以用手触碰但是也没看到有人碰;垂直兰花花园之前没注意官网上的说明,说明上写的是移动的时候兰花会上升让路,静止的时候会下降接近,但是实际体验起来感觉相应非常慢,而且排队人也很多,没看多久就走了;但是作为一个艺术设施体验还是很超值的,第一次看的确非常震撼

从teamlab出来决定去新桥吃饭,做海鸥线环绕了一圈台场/有明,运气好抢到了前排的观景作为,看到了一些一直知道但从来没见过的建筑(例如 tokyo big sight 原来这么大以及距离市区这么远);另外价格上也很超值,半个小时的车程(几乎坐完了全线也才不到400jpy),一路上拍了不少照片;另外每个站似乎都有自己独立的徽记,设计上还是很用心的;出站的时候甚至还有彩绘玻璃,很艺术

午餐:烤肉link新桥店,薄切五花肉套餐(大肉量200g,~1000cny);看[REDACTED]看到的;自己是第一次上手烤肉,一开始不太熟悉,烤了一两轮就开始有感觉了,但是在快熟的时候油脂会滴下去导致火势骤然变大,不小心的话容易烫到(我就几乎被烫了好几次,好在夹子够长,后来调小火似乎稍微有改善);不过五花其实烤焦了问题也不大,甚至还有独特的焦香味;辣酱很棒,酱油尚可,感觉是偏甜的类型;一开始米饭选了大盛发现不够,后来又追加了一碗普通盛;总之吃的很开心,很棒的一餐

吃完了决定去新宿转转,主要是想起来之前看到的3d猫;到了新宿才发现地下的空间很大,各种出口四通八达,但是找不到我想要看的猫的出口;最后从jr新宿东出来绕了一圈才发现,原来一开始的出口是对的,但是被树给挡住了;猫的确很萌,甚至还有不同的动画效果,裸眼3d效果也很好(实际上是2d,只是画面内的frame缩小了,物体可以超出frame,所以看起来是3d);周围也有不少人在看;有个动画中还可以和隔壁的屏幕联动

看完之后决定去附近转转,地图上看到有家muji,不过发现卖的东西和国内差别不大(虽然商品多了些),依然是没啥想买的;后来又去旁边的歌舞伎町转了下,地下走了好远才到;和想象中的灯红酒绿差不多,不过应该因为是下午,街上并没有站街揽客的,只有一堆无料案内所的招牌(免费咨询),还有到处可见的禁止揽客的标志;拍了个标志性的歌舞伎町大门就走了

下面突发奇想想去下北泽看看,顺带圣地巡礼一波孤独摇滚;这次过去要转小田急线,考虑到之前在市内坐地铁都是站站停,所以这算是第一次做准急的列车了,不过除了中间不停站似乎速度上并没有快多少。到了看到附近有卖鲔鱼烧的,买了个试试,虽然外面烤的有些焦了,里面的红豆馅倒是依然很烫,略微有些甜但不是非常甜,还行;之后边走开始搜索圣地巡礼指引,路过一个路口的时候发现就是图上的位置,于是开始拍照;livehouse藏在小巷子里不太好找,第一圈没找到,第二圈才注意到,而且旁边还摆了雪糕桶围起来,告示说不要进去拍照太多人投诉了;旁边的售货机看起来和动画里的不太一样,可能是白天的问题;拍照的背景墙找了好久,也是第二圈才发现得绕到里面去,不在主路上;不过总体上还算好找,毕竟区域本身并不算大

拍完了决定去吃饭,虽然已经定了是egg bomb,但是之前中午已经去过新桥了,所以想去池袋转转;车上刷小红书发现附近还有家animate本店,决定去瞧瞧;到池袋的第一感觉是人很多,应该是赶上了下班和放学高峰,所有人都在面无表情的赶路;到了之后还是扫楼,不过没啥想买的,[REDACTED]周边感觉和国内的价格比太贵了(而且周边上标的也是中文不是日文…);想买个[REDACTED]的周边,但是badge又没地方放,钥匙链感觉贵了点而且没有想要的形象,最后还是理性购物不买了;之后去池袋西口转了转,传说中只用说中文的地方,但似乎和一般的街区没啥区别(多了个茅台广告?);池袋西口公园似乎也就是一个环形剧场,没有什么特殊的

车站里买了seventeen ice,感觉除了冻得硬了一点(-23度),感觉上和普通的甜筒没什么区别;而且差点包装都撕不开,完全冻住了

本来计划是在池袋吃 egg bomb,但突然想去skytree下的墨田水族馆([REDACTED] sakana!)圣地巡礼;另一个原因是手机电量不太够了,充电宝也不顶用,估计早就坏了);小红书上说这家水族馆虽然小但是体验很好,看了下八点关门,六点过去四十多分钟应该还赶得上,就决定冲一波,看完之后先回酒店给手机充上电再去吃晚饭;路上手机还在掉电,于是开了极限省电模式,只能用部分应用,留了微信、地图、相机,电量下降速度总算是慢多了;本来直接就到终点站押上了,但是中途看错了标识提前一站下了车,第二趟车本来想上去,但是因为是和下游线路直通的特急,车上全是人挤不上去,于是放弃等下一班各站停车了,果然各站停车的人就少多了;到了押上,又绕了好久才找到水族馆,原来要先出商场再走一段才能到;到电梯口看到旁边标识赫然写着19:00最后入场,一看已经18:59了,飞速电梯往上冲,好在是在停止售票前买到了票(后面还有一堆母子,也买到了)

水族馆内和预期中的差不多;虽然大部分文字都只有日文版本,少部分有英文翻译,不过赛式翻译法瞎猜还是能看懂不少的;有个两层楼高的大水缸,上层和下层都可以坐着看,下层还有一个小cafe,总算是找到了[REDACTED]里的实际场景,[REDACTED];金鱼、企鹅、水母都不错,文字和解说都很细致;企鹅甚至还有一个专门的关系网路图,连前夫妻/前男女友都给标出来了,很神奇;在水缸前坐着看了会鱼,还是这种环境能够让人完全放松下来;看完水族馆去skytree底下转了转,原来不同的天数会换不同的颜色,各自有含义;skytree底下还有电子版的东京清明上河图,内容上做了一些魔改不过倒是很有浮世绘的味道;最后又在底下拍了几张skytree走人

晚餐:egg bomb 新桥店,周一双倍炸鸡蛋包饭套餐+牛肉饼,1400jpy。去的路上没注意坐反方向了,不过反正地铁网络四通八达,只要愿意走还是可以完成换乘的;虽然店面其貌不扬,不过上来之后视觉效果是真的震撼;番茄酱调味很合适,有些酸甜但不会过度,很适合下饭;炸鸡没有骨头,可以放心大胆吃,肉质也很鲜嫩不老;牛肉饼差点意思,可能也是我期望太高了,就是正常牛肉饼的味道;总之吃完了很满足,爽

话说每个站都有"全网制霸"的标识,大概是真的有什么活动?

吃完了,回酒店,看了下洗衣机果然还是被人占着,决定去之前看得洗衣店洗衣服,走过去大概八分钟(包括等红灯),选了4kg+额外10分钟烘干,800jpy,有点小贵,不过酒店就一台洗衣机而且得两小时,实在是排不上;这个预洗的功能还挺好的,洗的干不干净再说,但是心理上安心多了,感觉大学的洗衣机应该补上这个功能;可惜付款只能现金,刷不了交通ic;网页似乎有延迟,可能是五分钟还是十分钟才更新一次;时间到了之后还显示cd,又多等了六七分钟;洗完发现有个裤子忘记了,下次再说吧

另外这两天虽然一直在喝茶,但是依然有些[REDACTED]。晚上买了瓶野菜汁,看看多弄点纤维是否会有改善。

day 3 东京

昨晚睡的比较晚,快十点起来的时候还想睡,但是十一点就要退房了所以还是坚持着起来准备收拾东西

收完之后退房,大箱子先存在了酒店柜台,主包放在了昨天看的附近的存包柜(箱子看起来深度不够,所以还是放酒店了)

看了下地图,决定先去圣地巡礼[REDACTED];参考的是一位日文博主的巡礼攻略,文章虽然是十年前的,不过好在地标和店铺没怎么变;最后的地点附近有好几栋楼,看起来和作者的描述都挺符合的,索性就都拍下了吧

然后决定去秋叶原,先吃个早餐(午餐?),然后去骏河屋转转

早午餐:花丸乌冬面,油豆腐乌冬中盛+炸虾(780jpy);一开始拿完餐之后直接坐下了,后来被店员指引才发现原来是先付钱;味道本身和预期中差不多,和国内的乌冬基本上一样,不过可能稍微更有嚼劲一些;比较诡异的是油豆腐,居然是偏甜的,有些不太适应,但还是吃完了

吃完去找骏河屋,google地图在小巷子里容易定位不准,地图上显示到了但是就是看不到,回头才发现原来在后面;之前只是听过骏河屋这个名字,但并不知道店铺的内容,以为就是一般的手办店;到了之后才发现是个二手店;五楼以上是成人相关就没上去了,下面四层楼略微转了下,手办和各种周边很多但是都比较散,慢慢翻的话可能会有收获,不过略微转了下没看到自己感兴趣的,就撤了

然后想起来昨天差点遇到手机没电的窘境,决定去补个充电宝,于是就进去路过的big camera翻了翻;感觉买个太贵的又用不上,选了好一会,最后选了个相对比较便宜但是容量还足够大的,5000mah 2700jpy 2a1c 12w,包装上还声称已经预先充好电了,暂且就先备着吧

话说退税需要5000jpy起,估计我这次全程都不会单独有超过5000jpy的消费了,所以大概是用不上?

之后去地下铁博物馆,在江户川区,过去差不多半个多小时;虽然稍微有些偏远而且看起来面积不大,但是十分有趣,真正讲到了地下铁的各个方面,从历史到建造再到运营,里面甚至还有地铁站的抛面图、实物模型车表演和地铁驾驶体验;纪念册盖章也是我的心头好,章的设计也很有趣,并不是那种一看就是一个模板改个文字的;入场券和入场检查和地铁的实际票几乎一模一样;中间甚至有实际的地铁运行状态展示(东西线);模型车表演里甚至可以用按钮来实际启动站台上停靠的列车;模拟驾驶虽然没太听懂解说,但是靠直觉还是能理解启动和刹车,开了一站路,前方展望视频真的会根据速度调整;慢慢边走边看,疯狂拍照录视频,时不时还翻译下看看具体文字,整一趟转下来也快两个半小时了;超开心的体验

文字内容上因为这里技术名词多,所以日文描述中的汉字也很多,甚至即使不靠翻译也能理解个七八成

纪念品店居然有以车站站牌为原型的尺子和手掌胶带,忍不住买了把尺子支持下,设计太神奇了

话说去的时候真的遇到了因为运行混乱(似乎是哪里的换线器有问题);结果就是站台上直接不显示列车时间了(一般会显示下一趟和下下一趟),不过列车快到站还是会正常有提示的

从地下铁博物馆出来已经快三点半了,回酒店拿了箱子再取回包(包存了5h 500jpy),就直接往东京站出发了;东京站地下是真的大,光是从地铁站台走到中央检票口就走了好久;一开始找到了一台标着smartex的机器,扫码却提示无法处理;后来找到了另外一台标记着ex取票的机器,扫完也是没法处理;后来才发现得实时生成取票二维码,之前打印的已经过期了,重新手机登录账户再生成之后就正常取票了;不过一次吐出来两张,一张长一张短;自己以为是基本票+特急票,尝试着两张一起塞进闸机发现报错;找旁边的站务问了,原来长的那张是收据/报销凭证,实际的车票是短的那张;进去之后看到了买ekiben(车站便当/駅弁)的店铺,虽然有不少选择但是第一眼都没什么想买的欲望;后来考虑到冷食可能比较奇怪,还是选了个自加热的牛肉饭便当;付款的时候发现居然可以扫微信付款码(虽然并不在标记的支持列表中)

本来想直接上站台,但是发现时间还早,还有四十多分钟,于是先在休息区休息了一会;然后才发现原来东京站付费区还是分层的,一开始进入的是基本付费区(?);里面可以换成jr的通勤线路,如果要进入东海道新干线还要再过一个闸机;闸机内有个休息区但是全是人,看了下电子屏幕,发现要做的车已经是站台上的下一班车了,就直接上站台等了;先是看到新干线进展,乘客下车,然后保洁上车做清洁,顺带把椅子反过来;最后发车前的五分钟终于开始让乘客登车了;原先以为座位后的大件行李存放处会有固定装置,但实际上并没有;起初把箱子直接正常放着,车启动之后听到后面叮叮咚咚的,到下一站才发现箱子已经滚出来差点挡了其他乘客上车的路,这才观察到原来其他乘客是把箱子横放的,于是如法炮制

新干线本身和预想中的没什么差别,不过乘车体验上来说应该比国内高铁略微差一些,应该是建造年代的问题,路上可以感受到很明显的抖动和倾斜;本来想看了富士山再吃饭,但是外面天已经完全黑了,眯着眼睛看了半天什么都看不到,就直接放弃开始热饭;一开始差点把包装上的固定绳拆了,快拆完才意识到应该留着绳子作为蒸汽的屏障避免烫手;拉完绳子之后很快表面就热了起来,不过比较神奇的是不像国内的自热包那样有很明显的水蒸气出来,味道上也很淡;按照说明等了八分钟,打开一看里面已经到了正常饭菜的热度,撒了酱汁之后发现味道意外的还不错,可惜就是量稍微少了一些(应该是自热包占了不少体积所以饭菜没这么多了?);后面行程也没什么异常,旁边的座位一路上都没人,所以可以独占插头充电了;本来想着路上放松下睡会的,结果一直在看小红书上大阪京都相关的笔记,最后还是醒着到了新大阪站

到了新大阪后,跟着小红书上的攻略找了旅游咨询处想买一天周游券,到了发现虽然柜台有人,但是说8点就停止售卖了(这么早?);那就直接去酒店吧,电梯上发现大阪似乎是靠右?神奇(东京是靠左);下到地铁站台,发现仿佛穿越回了上个世纪,站台上没有屏蔽门,连下一趟车的预计到站时间都没有,只有在列车快到的时候有提示;看google maps车辆间隔有十分钟,这可是周二的晚间八点半,这个行车间隔实在是不忍吐槽;好在至少官网上还可以显示列车位置,有一点点智能化但不多;到了酒店所在的站,才发现是两个地铁站拼在一起成直角的构造,得穿过一整个站台才能走到目标出口(国内至少还有换乘通道了)

好不容易到酒店办完了入住,房间倒是不错,可惜是插卡式的,所以每次出门再回来都要重新打开空调(这么一想东京的那个酒店还挺人性化的);比较神奇的是可以从电视看到浴场和餐厅的拥挤状态(虽然似乎不太准);看了下时间发现刚好有免费的夜宵(葱拉面),于是上楼吃夜宵,电梯里人还不少,甚至还得拿着叫号器等

甚至连牙刷都得自己从一楼拿,默认不放在房间;理解是环保不过这样似乎有些太过了?

夜宵:葱拉面(免费),味道还行,不过等的稍微有点久(~15min),量比较少,但考虑到作为夜宵的定位没什么问题

吃完了决定还是探索下周边,顺便看看有没有办法把周游券买了;查了下官网说是地铁站内有卖,但我来的时候并没有见到;走到了地铁站绕了一圈,自助机器倒是有地铁一日券卖,然而没有周游券;罗森上似乎有个相关的告示招牌,但是已经关门了(才10点多就关门了?);最后找站务问,发现还是得去站长室,不过好在最后还是买到了(2800jpy);回来的时候下起了小雨,但是出来的时候并没有带伞,好在距离不算远所以淋湿的不多(但是这红灯也太长了,而且一路上好几个红绿灯);便利店买了点野菜汁,顺便买了瓶罐装酒([REDACTED]里[REDACTED]喝的试试)

虽然有酒味但是很淡,主要还是柠檬的味道,大概酒精的感觉完全被气泡和柠檬遮蔽了(这可是9%的酒精度数),知道为什么不知不觉就可以喝的很上头了;一罐还没喝完,才过了十分钟不到就开始有反应了

中途路过一家familymart,进去转了一圈发现货架基本全空的,商品也特别少,出门才发现原来这家店要关门装修了,这是营业的最后一天

回酒店之后决定去泡温泉,先洗了个澡,然后找了下攻略了解了一下一般的规矩就直接上去了;脱光之后一开始感觉还有点奇怪,不过进去发现大家都裸着也就适应了;冲完之后进了最近的一个池子,水温估计得40左右,体感上就是一个更大的浴缸(或者说一个更小更温暖的游泳池)?虽然的确是有点放松的感觉,但是更多的是"有点烫"以及"人好多"(实际上可见范围内的就五六个,不过之前没有在这么多人面前裸着);泡了一会(10分钟?)决定起来试试别的;有个池子是冷水(估计是桑拿用的?现在还没这么高段位就不挑战了);室外的池子温度计上是42,但是体感上有些太烫了,试了试就撤了;出来吃了个冰棒,走回房间发现的确还挺舒服的,明天再去试试看吧

day 4 大阪

上午睡到十点,出门已经十点半了

早/午餐:yayoiken 目玉烧朝食(480jpy?),住的酒店附近的店;蔬菜沙拉、香肠和流心蛋都和预想的差不多,但是这个味增汤比想象中的咸好多,可能正确做法是配着海苔慢慢喝?(我是直接把海苔全干完了再空口喝的);豆腐是冷的而且没调味,或许应该自己加酱油来调味

吃完出发去大阪城公园,根据小红书上的攻略从大阪商务园区站出来,先走到游船买票处预约观光船,到的时候快十二点,预约了一点半的船;然后开始慢慢往里走,边走边拍照,天气还不错,一开始有些阴后来除了太阳,不过有些逆光,所以主要拍的景没怎么自拍;大阪城外层就是想象中的日式城堡的样子,可能石头更大?;然后走到了天守阁,凭周游卡可以直接进,所以不用在售票处入口排队了;进去了发现里面还有队,原来都是坐电梯的,于是放弃电梯决定直接楼梯从下往上走;经过一楼的时候想起来宣传册上写了有语音讲解,抱着试了试的心态一问发现居然是免费的;后来就开始慢慢一层一层转,语音讲解还是很有用的,毕竟无论是日文还是英文文字量一大就失去了仔细阅读的耐心,就想着看个大概了;看了历史才知道,原来[REDACTED] 用的是这里的典故、除了夏季之战还有冬季之战、丰田秀吉原来是主角剧本;转到一半的时候发现似乎时间似乎比较赶,但是还是把所有录音听完了;上到顶层可以向外展望,因为本身有城堡的底座高度+楼的高度,视野还是很不错的;下到底部还了讲解机,发现离开船就剩6分钟了,于是一路飞奔,中途下台阶的时候因为石梯不平差点把脚扭了,好在没出什么大事,最后也赶上了游船,似乎是倒数第二个上船的;船上也和预想的差不多,绕着护城河来回一圈,所谓的人面石也看不出来人或者鬼的形象,但反正拍照就对了;下了船买了根 seventeen ice的冰棒,总算和油管视频看到的差不多了,圆柱形的内容物中间是个支撑用的塑料棒,不过可以很明显的尝出来葡萄味道的人工味,但看在冻得足够冰这一点上似乎也还行了

跟着公园指引上的路线,倒着走一般的游览路线出了公园,发现已经是下午两点多了;打开地图分析了一下,原先计划先去难波把道顿堀的游船预约了,再去梅田展望台和美术馆,最后晚上赶在八点之前去通天阁,但是考虑了下时间和来回车程感觉还是做不到,再加上[REDACTED],手机电量也比预想中消耗的快(而且带了充电宝没带线),于是决定先回酒店涂药+拿充电线修整下,再先往梅田方向出发;走了一段发现手机的陀螺仪指向有问题,和实际方向是反的,所以多走了一段,好在发现的早;回到酒店的路上才发现原来大阪城公园离酒店也不远,一路走就可以走到了甚至不用坐地铁

回酒店休息了一下,然后决定先往梅田展望台出发,因为有16点前到达的硬性限制(16点之后就得付钱才能入场了);跟着地图做了一站地铁,换乘的时候发现不太对,要换乘的是jr环状线,不包含在周游券里面,于是又折回去走了另一条线路;到了大阪站才发现有点坑,梅田蓝天大厦离大阪站还有不少距离,中间得经过一个硕大的工地,而我出站的时候已经3:35了,于是一路快走,3:47总算到了展望台电梯前开始排队;这里就不得不提到一个很坑的点了,排队的位置是三楼,但是实际买票的地方在39楼,得先直升电梯上到34,再扶梯坐到39才能买票,而这个16点的时间限制是按照买票来计算的,所以得在16点前到达检票口才可以;怀着焦灼的心情等了好几趟电梯,好在最后在58分到了检票口,算是有惊无险地进去了

上去之后景色还是很震撼的,视野内没有其他更高的建筑遮挡视线,再加上平原地形,周边一望无垠;看了一会景色之后决定买点吃的,买了500jpy的抹茶圣代,吃了一半才发现底下居然还有麦片,虽然很奇怪但是也不是不行,感觉还挺奇妙的;4:45本来准备下去了,走到出口才发现上面还有一层,原来可以直接到室外拍照,完全不受玻璃的限制;此时已经夕阳逐渐落山了,景色更棒,不过手机排不出来,只能感受下了

从展望台出来去美术馆,发现在另一栋楼,得坐一般的电梯上去;进去之后还得存包,这算是我第一次用100日元存包柜,一开始以为是先关门再塞钱,心想怎么一直塞不进去,后来看了说明才知道是弄反了;进去的第一个展品是两个3d电影,的确有景深效果,但是内容上太奇妙了无法用语言描述,虽然建模比较粗糙但是的确有2d绘画分层3d化的感觉;后面开始看展品,才发现这是个日本国宝级别的艺术家,而且平面、雕塑、动画3d都有作品;不过自己没什么艺术细胞,因此也没太看懂,反正就囫囵吞枣快速过了一遍吧

美术馆出来决定去坐摩天轮,在大阪站的另一侧,中间得穿越迷宫一样的地下空间;在附近的全家买了个无骨鸡胸肉,发现意外的鲜嫩多汁,比上次吃的都还好一些,是会回购的等级了;走到了才发现不是个独立的摩天轮,而是在商场顶楼的;排队人挺多但是摩天轮因为一直在转,所以人流速度还是挺快的,招牌上写着30分钟其实15分钟也就排到了;进去的时候天已经完全黑了,趁这个时机看看夜景也挺棒的,逐渐上升的时候的确会有一种越来越远离尘世的感觉,然而过了最高点之后就又落回去了

摩天轮坐完已经六点半了,决定去道顿堀碰碰运气,小红书上说七点半去的时候游船就已经预约完了,这个点去不知道是否还有位置;到道顿堀已经七点了(因为一开始出站的时候又走反方向了),不过好在还有位置,想着先吃个饭再坐游船,于是预定了8:40的船票;随后开始在附近找吃的,大阪烧/御好烧算是之前就在列表上了,不过只是定了食物的种类而没有确定具体的店铺;小红书上一开始找了一家,但是到了发现已经完全预定满了,于是又在google地图上再找了一家(三平),这家倒是没法预约只能到店等位,幸好到的时候队列还是空的;边排队边刷小红书,发现似乎我这家风评还不错,于是更加期待了;7:40总算进了店,下单的时候外面还在排队所以店员说只能下一次单,不能再追加,于是点了

和家里人出来的话,就得完全提前做攻略,毕竟失败的话损失太大;自己一个人的话反倒没什么所谓

晚餐:炒面+鲜虾/紫苏/起司御好烧,三平,2210jpy;店员操作行云流水,在面前操作的确很震撼;可能是太饿了炒面感觉很香,用的是甜酱油;大阪烧本体和预期的不太一样,可能是面糊比较稀,不太像披萨饼,更偏向胡辣汤的粘稠度,但是味道依然很棒,起司也能拉丝;自己用刀切块吃的操作也比较有趣;最好的是里面的鲜虾,已经完全熟了,甚至有虾的清甜味,总体大好评

吃完了去坐船,也就在道顿堀(这个水域)的几座桥之前往返;船长倒是一直在烘托气氛,可惜对我这种I人来说有点太超前了,路上的人看到船上的其他人在挥手倒是真的会挥回来;游船坐完本来想着去吃个炸串,找了一家小红书上评价不错的,但是看菜单进去得点套餐不能单点比较劝退;另外倒是有一家能单点的,但是在楼上环境有些诡异,而且店员一上来就上了盘毛豆,有些担心是否强迫消费,于是提前跑路了;考虑到今天也吃了不少,大概这个时候结束还比较合适,再吃就撑了,就决定回酒店了

回酒店路上买了青汁(野菜汁似乎[REDACTED]效果还是不行,换个类型试试),还有familymart的炸肉丸串,可惜味道没有无骨鸡胸肉那么惊艳;回了酒店后还是去吃了碗面,今天计划再去泡个汤,还要带上零钱买牛奶;跑的时候发现里面也是42度;泡完出来发现别人把拖鞋穿走了,迷惑;牛奶还行;发现漫画处居然有四叶妹妹,选漫画的人品味不错

day 5 京都

早上依然睡到了十点,出门的时候依然十点半;决定直奔京都,虽然小红书上建议走JR东海道新干线或者阪急,但是地图上似乎京阪本线离我住的地方更近一些(虽然得走一站路,~1km);虽然起点和终点都在一条线上,不过坐各停一路站站乐还是慢了些,地图指示是先坐急行到附近的大站点再换乘慢车;急性车辆就更接近高体的感觉,2-2的布局;幸好出发的站比较早,选了个靠窗的位置,后面的站上来的人就只能站着了;全程大概半小时多就到了伏见稻荷站,只用了420jpy(google 地图给出的下车指示在前一个站,但那样绕远了)

早餐:familymart 无骨鸡胸肉(~200jpy?)

出站之后稍微走了一段就到伏见稻荷大社的入口了;这里比起作为寺庙的浅草寺,就更突出作为神社的特性了,无论是鸟居的数量还是随处可见的狐狸石像;进去不远就可以看到千本鸟居,看了旁边的介绍才知道原来千本是形容数量多,并不是确切数量;但是实际上数量可能也差不多了,至少就这个密度和长度而言几百个是有的;游客非常多,好在我拍照只是留念,并不要求出片,所以随手拍就是了;到了神品处,还是没忍住买了个御守(400jpy),设计上比浅草的好看一些,更接近番剧里对御守形象的描述;其实后面还可以登山,不过今天还有其他行程,主要看了下鸟居和大殿就撤了;出大门的时候看到了不少排队的学生,看来京都作为春游/秋游的目的地还是很真实的,柯南里的春游设定也的确很符合了

午餐:京都站 面将蘸面(鸡肉+鱼肉,大盛300g) ~1050jpy(麺匠 たか松 京都駅ビル拉麺小路店)

然后决定去吃午饭,昨天小红书上看了似乎京都的蘸面不错,决定试试;地图上看了下最近的点在京都站,就直接去JR站台坐车过去了;位置比较隐蔽,仔细看了地图才发现是在拉面小道(餐厅集合体里),应该正好是午饭时刻所以得等位,不过这里和一兰不一样,是先买票拿券再去排队的;大概五六分钟很快就排上了;面上的也很快,上来是一碗汤+一碗面,吃的时候把面夹到汤里沾着吃;不过我自己更接近泡着吃了;面条很筋道,吸收汤汁的能力也很强;汤里也有很足的肉鲜味,一开始上的时候汤的热度激发出香气,十分惊艳;可惜后来吃着吃着汤渐渐凉了,看到其他食客的操作才知道原来可以找店员重新热汤(实际上就是放到微波炉里转一下);热了之后香气又回来了;后来快吃完了才发现原来碗底有肉块,应该吃的时候边吃面边吃肉;吃完了面试着喝了点汤,用勺喝还不错,直接对碗喝可能就太腻了;吃完感觉心满意足,总体评价很不错

从面馆出来,走到外面发现是个巨大的阶梯(实际上在10F),从外面可以一路往下走;虽然是中午,但是太阳躲在云后面,风也比较大,穿着短袖反而感觉有些凉(外套在酒店没有随身带着),算是有些秋天的感觉了;往下走的时候可以真正感觉到京都站的震撼,外面看着其貌不扬里面居然这么大;出门就是标志性的JR京都入口,和[REDACTED]里的完全一样;去了个洗手间之后,决定下午去清水寺;原计划是先去金阁寺再去清水寺,但是金阁寺太远了而且似乎除了主庙就没啥好看的了,清水寺还可以把旁边的一堆街区顺便逛了;从京都站过去要坐公交,公交站已经排了一些,应该是上一班没走多久(似乎因为京都市政府一直没什么钱,也有可能是文物相关的因素,京都的地下铁路不算很发达);车总算来了,这里是后门上车,前门下车,单次乘坐一口价;游客数量很多,京都站是我做的这趟车的出发站,结果出发站就已经满员了,后面的几个公交站虽然勉强挤上来了几个,但是更多的乘客挤不上来只能在下面等,然而后面的巴士估计也是一样的状况,不知道一般的市民得等到什么时候才能上车…;

下车转了个弯,没走多远就可以看到清水寺的红色塔尖了;正是下午时段人非常多,不少路上的大巴车上都标记着学校的招牌,估计也是全员秋游;慢慢往上走的时候看到其他人在吃烤团子,于是也买了个抹茶烤团子(300jpy 5个,后来发现买贵了);买到之后发现其实各种味道的都是同样的团子烤出来的,只是烤完出货的刷了不同的酱;团子本身的材质比较像年糕?;我选的这个抹茶味道还可以,正好中和了团子本身的甜腻

再往上走很快就到了入口,然后才发现自己是从侧面进来的,正面入口的人更多;逛完了外面的一圈建筑,才发现原来里面还要再收一次费才能再往里走,那来都来了就买个门票进去吧;里面转了一圈,感觉就是建筑很古老、很漂亮、维护的也不错;因为建在山上,往外看的时候视野也很棒,正好这会云也散了,向远处望可以看很远;依然一路边走边拍照;有一段山路的两侧种满了树,风吹过枝叶的时候有沙沙声,阳光也被叶子分散了,虽然前后不少游客但竟然有种宁静沉下心的感觉;路过神品店,发现这里的学业御守居然是个圆柱形的,类似于铅笔的感觉,比浅草寺的好看不少,于是又买一个(400jpy);转完一圈又回到了入口,这次决定走正门正对的道路下去

这就走到了三年阪,二年阪就在旁边,实际上就是国内古庙旁边的商店街,不过这里是真正的古迹而不是仿造的,所以感受上会好一些;出来的时候下了点太阳雨,在旁边店铺多雨的时候顺带买了个抹茶冰淇淋(430jpy),分量还挺足的,抹茶味道也不错(但肯定没有真正的抹茶那么苦);主路上人依然很多,往小巷子里走会好一些;考虑到是景区,还是有不少的特产店的,但是因为不想拎着个大袋子走来走去,所以决定旅途中间不买,要买就最后一天一起买(其实更像是不知道买什么所以延迟决定…);不过说会这些特产,抹茶相关的看起来卖的都不错,无论是作为饼干夹心、巧克力夹心还是糕点、羊羹似乎都不错,或许可以作为备选;后面逛到主路上,就趁机结束了,因为本身也没什么买东西的想法,逛商店街对我来说还是挺无趣的,虽然外国的商店街上还是的确有不少有意思的东西

出来之后继续往八坂神社走,这里就更偏向传统的神社了,有那种系着铃铛的粗绳,投了硬币之后摇响铃铛然后拍手许愿(至少[REDACTED]里是这样展示的),于是丢了个五元硬币照猫画虎了一下;出来之后正对着的就是花见小路,不过这大概也是古地名,现实中就是一条普通的商业街了;走到尽头就是鸭川,作为城市中心的河流水质居然意外的好,河岸两旁的草地上散落着不少情侣和游人(想起了鸭川等距离情侣的典故);决定下去沿着河岸走走,走近了发现河里还有鸭子在飞;河岸路上不时有骑着自行车的人和慢跑的人路过,远望还能看到前后都有不少桥梁,看来当地人也是很喜爱这条河流的;路过了一个卖唱的歌手,于是把兜里塞不进零钱夹的10元硬币扔给了他;从鸭川上来已经走到了三条,发现本能寺就在附近于是就顺路进去转了转,入口居然在一个商店街里面,到的比较晚里面的展示厅已经关了,于是就外面简单转了一下,门口还有雕像和立柱;话说逛旁边的商店街的时候,会想商店街难道就是有顶棚可以防雨的店铺街道?但是似乎有的店铺高度很低的话,雨还是会飘进来?比较迷惑具体是怎么定义的

晚餐:炭火烧 烤鳗鱼盖饭 3/4 ~4180y(本格炭火焼うなぎ えん 京都三条店)

这家店是在google地图上先翻到,然后查小红书看到评价不错才确定的;位置比较隐蔽,得从楼梯下去才能进入;本来一开始是3000jpy的预算,但是想着来都来了于是点了更高级一些的版本(基础版本就是鳗鱼+饭,高级版本带汤汁和一些配料,可以自己搭配);因为是现烤的所以上的有些慢,但是上来之后第一眼还是很震撼的,饭上摆了两层的鳗鱼,而且还有烤出来的焦香,饭的上层已经浸透了鳗鱼的油脂;调味上可以尝到甜酱油的甜味,但是剩下的就是鱼肉的味道;感觉上似乎有些小刺,但是因为烤的足够旧刺已经软化了(虽然自己还是比较小心慢慢吃了);试了下海苔+鳗鱼和茶泡饭+鳗鱼,似乎都还可以;最后的结论是味道还行,但是没有让我觉得值这么多钱的感觉,而且吃起来很耗时间;下次就算要再点估计也会点更便宜的低配版本了,这个价格还是太高了

吃完天已经黑了,本来想着直接回大阪,但是想到要路过伏见稻荷,就决定再去看看夜景;到了之后发现旁边的店铺已经关了不少,人也比白天少了很多(但是还是有一些);神社本身不会有特别亮的大灯,照明基本靠路边比较低的小灯和低矮柱子上的灯,所以看起来有些阴森;走到千本鸟居的时候,因为光都是从底部往上打,就更有一种阴暗的感觉,不过其实旁边人很多所以还好,但是如果真的是半夜一个人来估计会很恐怖;不过这里的确让我想起了原神里的稻妻寺庙夜景,场景上原神团队还是很还原的

从神社出来就正常坐车回大阪了,还是得先去换乘特急,虽然一路上没有座位,但好在车程也就半个小时不算太长,总比站站乐坐着一个多小时回去好;回去之后一开始想买零度可乐居然在familymart还没找到,后来换到了711才找到;今晚决定把衣服洗了收拾下行李,晚上估计可以早点睡,明天就稍微逛逛买点东西就准备跑路了(还要给行李称重);这里就剩4kg了,估计得好好考虑,应该还是甜品为主?

day 6 大阪-香港

虽然昨晚睡的比较早(0点之前),但是早上还是很困,依然是九点半左右才起床,收拾下东西checkout已经是十点半了。今天是最后一天,考虑到需要为登记留出时间,所以并没有安排什么行程;寄存的时候给行李称了下重量,才13.4kg,还是有不少剩余给伴手礼的

[REDACTED],最后算算大概多留出了400g空间,然而最后实际上并没有用上,就算是个心理安慰吧

早餐:familymart 无骨炸鸡(200jpy),还是一样美味,因为早上起的比较晚就随便对付下吧;另外顺带把酒店送的布丁(实际上已经被我放了两天)给解决了,味道还行,稍微偏甜的口味;难得在十点之前起来了,也顺带去和了免费的乳酸菌饮料,其实就是益力多,和国内的味道没有区别,也算是意料之中了

上午决定先去天满宫,距离并不算远,出了地铁很快就到了,和其他各种隐藏在城区里的寺庙没什么不同;到了门口才想起来忘记涂防晒了,又现场补了一下;门口看了下参拜指引,才知道正确的顺序是赛钱-拜两下-拍手许愿-最后再拜一下,不过既然是外国游客相比神明也会谅解的吧,或者说神明压根就不在乎?;逛到神品处发现一个学业御守居然要3000jpy,那还是算了;到门口看了看公告栏,发现这里面居然还有面向公众的兴趣类课程,涉及书法、神道啥的,比较神奇

出来之后去了通天阁,出地铁就可以看到,塔本身并不算高,就是一般的带展望台的铁塔,虽然可能当时建造的时候算高,但是现在摩天大楼已经不少了;地图上可以看到塔周边的道路呈圆心放射状,颇有巴黎凯旋门的感觉;商店街人并不算多,可以慢慢接近通天阁,这会太阳也出来了,照在塔上的反光有些刺眼;走到塔底才发现底部不是实心的,中间留了一条道路给车辆通行,底部网上看甚至还有壁画;绕着塔走了一圈,总算看到了之前在周游券说明上见到的滑梯,其实就是从几层楼的高度旋转着绕着柱子滑下来。因为本身不高,加上看了小红书说上面的景观一般,所以就不另外花钱再上去了,拍了几张照片了事

午餐:元祖串???(大阪新世界店),9串+魔芋牛腩套餐,1780jpy;之前一直听说大阪的烧串和国内的烧烤不一样,之前在道顿堀的时候因为之前吃的太饱就没去吃,这会看到通天阁下面刚好有同一家店铺的分店,就刚好来试试;和一般的路边烤串点不一样,这里实际上还是每个人有自己的料碟和蘸料,自己吃的话就无所谓蘸几次了,如果是公共的蘸料的话就不能二次蘸(不然就把自己的口水也加进去了);炸串上来的很快,吃起来实际上更接近天妇罗的感觉,应该是食材外面裹了一层面衣(天妇罗是面包糠)然后高温炸;里面比较惊艳的是炸虾,分量上足够大只,味道上清甜吃起来也很有口感,应该用的是鲜虾而不是冻货;另外比较不错的还有牛肉饼?和鹌鹑蛋;酱汁依然是甜酱油口味,但这种情况下解腻也正合适;总体评价还行,但是不太顶饱,如果要吃饱估计得再追加点主食

吃完了之后按计划去心斋桥买东西;小红书上有人推荐了一家卖零食伴手礼的店,地图显示下了地铁还得走一段(虽然有心斋桥地铁站,但是似乎和通天阁所在的线路不是一条地铁线);一开始出地铁就是普通的街景,地图上越来越近但是却看不到心斋桥的影子,还在嘀咕是不是走错了;突然走到顶棚之下,这才发现已经到了心斋桥商店街,两侧望去都是商店看不到尽头;原来自己是从商店街的侧面进来了;虽然店铺就在眼前,不过来都来了决定先逛逛,于是就随便找了个方向开始往下走,心想走到尽头再绕回来看看到底有多长;路过街口的时候发现有正常的单行道路会横穿商业街,因为没有红绿灯的存在,司机和人纯靠默契,运气不好车得等好一会;走了好一段,过了好几个街口,总算看到了一个红绿灯,却发现对面还是心斋桥的标记;那就接着往下走吧,总共走了大概二十多分钟,第二个红绿灯一过,发现自己居然回到了道顿堀,正好是4号大阪第一天网上到的地方;来都来了,就顺带拍拍道顿堀的日间景象,依然是人流涌动;尽管河道上霓虹灯此时都熄灭了,依然很有繁荣的商业街道的气息

然后开始往回走,路上经过药妆店的时候心血来潮决定进去看看,边转边翻小红书,决定还是买点药品+化妆品回去,翻来覆去不知道买啥,给[REDACTED]买了点治疗疲劳酸痛的涂药,给[REDACTED]买了洗面奶+护手霜,还带了无比滴和润唇膏;然后直奔一开始的零食店,发现还是吃的便宜,买了kitkat的抹茶口味+草莓口味,还有各种抹茶口味的饼干,计划返工的时候带一些给[REDACTED],即使拿了很多最后也就一百来块人民币,而之前买了几瓶药就也是这个价格了;不过计划中的白色恋人没看到,后来查了才知道似乎大阪市内因为一些原因没有卖的,得去机场再买;不过小红书上翻了各种攻略,似乎都说特产店在安检后,那对我这种托运额度有剩余的人就不是很友好,所以还是先在室内多买点,到了机场再随机应变吧

因为中途上了几次厕所(大的,看起来之前喝的蔬菜汁/青汁生效了?)花了点时间,回到酒店的时候已经下午2点半多了;从前台取了行李之后把买的东西都扔进箱子里,最后称了一下15.4kg,所以其实要是想大买特买的话还是有不少空间的;出发去机场的路线昨晚已经提前看过了,先坐地铁到天满桥,然后换乘JR到关西机场;这里就不得不吐槽大阪地铁的阶梯特别多了,平常旅游还好,毕竟就带个小包,拖着行李的时候就很要命了;到了天满桥换乘JR的时候,下到站台在等车,突然意识到这个列车会中途拆分,一半去机场,一半去和歌山,然后发现自己站的位置实际上是和歌山方向的车厢,于是赶快往前移动(实际上不用太担心,就算真的上错了车厢,几乎每一站之间都会有语音和图形提示列车会分离,所以还是有调整的余地的);车来了之后里面果然已经坐满了人(2+1布局),好在站了几站之后总算找到了一个空座位;后面越接近机场的时候人越少,才意识到看来这真的是通勤铁路的一部分,而并不是专门的机场快线

路上花了一个多小时,到机场的时候才4:15;先在附近的familymart买了瓶水和pokey,还买了个和果子;本来想买无骨鸡胸肉的,但是这家店并没有;买的和果子是大团子,外层应该是糯米,里面是红豆馅,就是正常家常甜品的感觉;走到国际线出发才发现原来航班5:45才开始值机,于是决定先把晚饭吃了(后面会发现这是个正确的决定)

晚饭:蘸汁荞麦冷面,580jpy(nauru 机场店);地图上倒是看到国内出发层有个sonoju专门做荞麦面的,市区的点甚至还要提前预约,走过去一看价格2000jpy起步,于是就直接劝退了,还是回一般的连锁店吃吧;面上的很快,和各种番剧里的一样,竹席上铺着紫色的荞麦面,应该是过了冰水所以的确是凉的;不过看其他人的吃法礼一般都会配天妇罗,我这个套餐倒是只有面+蘸汁(这家店似乎不卖天妇罗);蘸汁应该是酱油,第一口下去怎么说呢,就是凉面+酱油的感觉,虽然也不是不能吃但是也没那么好吃,怪不得宣传上说有保健功效了,这直接吃还是不太适应;好在上来的时候上面撒了层海苔碎,最后还是勉强吃完了;总体评价算了吧,下次还是别单点面了,好歹配点热的东西

吃完了时间也差不多了,虽然提前去排队了,值机的人依然很多,17:45开始值机,排到自己办完手续也过了半个小时了;接下来就是正常的过安检+海关的流程,好在没什么问题;出了安检到安检后候机区,才发现很大一片面积都被封起来了,应该是在装修改造,只有零星的几个店铺;跟着人流很快就走到了一个伴手礼店,总算是有白色恋人了,买了一盒装包里随身带着;跟着登机口的方向往前走,竟然到了一个坐穿梭车的小车站,那就跟着进去坐车吧;到了之后发现更荒凉了,登机口附近除了一家特产店几乎全关门了;绕了一圈才发现,其实这整个是一个大航站楼,穿梭车只是把我们从航站楼的中心运输到了侧翼,其实中间的走廊还是通的,可以一路走过去;看到个familymart的指示牌,决定去碰碰运气,一路走过去之后发现还开着,而且还有无骨炸鸡卖,那就不得不再来一个了,依旧很鲜嫩(这大概会是回国之后最怀念的东西之一了);吃完了心满意足走回登机口,找了张带插座的桌子开始给手机充电;边充电边刷小红书上的香港半夜回深圳教程,看起来还得提前换点港币,但是一个人回的话似乎太亏了(250hkd+);试着搜了下香港机场拼车,居然真的有同一班的人,于是加了微信开始讨论拼车事宜;出发前再看了下台风小犬的动态,似乎香港和深圳还是在行进路线上,有的城市也已经开始停运了,只能希望航班本身一切顺利了;后面就是正常登机了,在飞机上就先写到这里吧,看看后续回家是否一切顺利

航班本身很顺利,甚至还提前了半个小时降落,不过从降落到回家一路就很[REDACTED]了;首先是下了飞机坐摆渡车到了机场大楼,然后到了香港海关入境处发现已经大排长龙,估计少说得有五六百人在同时排队了,原因应该是几个航班的降落时间重合了,结果所有人在同一时间挤到了同一个地点;让情况更糟糕的是里面还有来自台湾的旅客,似乎入境手续更加复杂,导致入境操作更卡了;排了半个多小时之后总算通过了香港入境,到了行李转盘,但是一起拼车的旅客还在里面排着,等他们出来又等了半个小时;人齐了之后取了港币,总算是开始往打车处走了,想不到打车处也在排队,好在人不算多,两三分钟就排到了;打车到落马洲之后,还要坐一个三分钟路程的巴士到皇岗口岸(话说这不是应该政府提供的接驳服务吗?就算十港币好歹提供下电子支付的方式?而且3分钟就要10hkd?),因为出租车司机没有找散钱,所以只能我们一起拼车的三个人一起上车(机场-落马洲-皇岗口岸总计 96hkd 90cny);好不容易到了深圳关,已经两点了;结果出口岸的路线是个天桥,只有台阶没有斜坡,电梯的队伍又很长,没办法只好自己提上去走到对面再提下来(话说这下面就是个大工地,和深圳湾当时的情况差不多,一种上个世纪客运站的感觉);下面的出租车招呼点也已经排满人了(目测前方至少80人+),想着找个网约车往外走,外面也是一堆人在排队;一开始在路口叫了一个网约车,但是对面信号不好一直弄不清楚在哪里,最后我放弃了取消了订单;后面往外走了一点再叫车,这次总算是叫到了,但是开过来得十分钟;好不容易等到了车,上车之后突然有电话打过来,才发现自己上错车了,两个车的型号一样,也都开着双闪,不过还是我自己没有验证车牌号+司机没有验证手机号尾数的锅;最后好不容易总算是上了回家的车,到家已经凌晨三点了,车费62cny;所以这里的结论大概就是不要坐凌晨的飞机从香港回深圳(或者回广州),会变得不幸,体验太糟糕了

总体复盘

正确的决定

错误的决定

中性的决定

健身房等待时间模拟

2023-09-29 15:34:23

七月中旬去了趟医院,在医生的建议下开始定期运动了。综合考虑工作日的时间安排和自己的身体状态后,暂且决定每天中午去健身房运动一会。目前每日运动就是中午去健身房踩20多分钟的椭圆机(顺带看一集番),消耗热量约320卡。然而虽然健身房里的椭圆机数量并不少,足足有10台,但是偶尔还是会发生去了没有位置,需要等人结束的情况,但是因为不知道到底要等多久,还是略微有些焦虑。于是在想,有没有办法量化等待时间,例如模拟计算下概率分布函数,所以有了这篇文章。

代码(基本是 GPT3.5 写的,有手动调整):https://gist.github.com/jerrylususu/2d8f7099a1c4af37160179b12ce13895

假设:有 10 个椭圆机,每个椭圆机上的运动者的运动时间 t_n 遵循均值为 μ,标准差为 σ 的正态分布。到达健身房时,所有 10 台椭圆机都已被占用,且每个运动者的剩余时间在 [0, t_n] 中均匀分布。等待时间为所有运动者剩余时间的最小值。

考虑到参数 μσ 都无法被准确估计,因此考虑 μ = 5/10/15,σ = 20/25/30,组合起来共 9 种情况;对每种情况运行一万次模拟,统计等待时间的 p50/p75/p90/p95,得到下表:

μ σ mean p50 p75 p90 p95
20 5 1.698 1.252 2.406 3.817 4.811
20 10 1.258 0.858 1.772 2.944 3.813
20 15 1.151 0.737 1.604 2.761 3.641
25 5 2.198 1.632 3.123 4.978 6.229
25 10 1.769 1.258 2.494 4.077 5.213
25 15 1.477 0.967 2.069 3.508 4.634
30 5 2.671 1.977 3.813 6.059 7.515
30 10 2.284 1.669 3.219 5.186 6.632
30 15 1.965 1.364 2.720 4.624 6.019

模拟结果可视化

结论:考虑所有情况,一般等 2 分钟就有 50% 概率可以等到位置,最坏情况下等 6 分钟也有 90% 的概率等到位置。

评论系统从 Disqus 迁移到 Giscus

2023-09-10 22:08:00

之前一直用的是 disqus,但是一来国内访问有时会有问题,二来新用户需要重新注册。考虑到大部分阅读本站的读者应该也是 Github 用户,迁移到 Giscus(一个基于 Github Dicsussion)的评论系统看起来更合适一些。切换评论系统本身并不难,参考这篇教程修改 hugo 的模板和配置即可。迁移数据也不算麻烦,毕竟没什么人评论,所以其实只有两条评论,手动迁移也就花不了多少时间(虽然也尝试了自动的方案但似乎有些问题,迁移过去的评论不显示…)。稍微有些烦人的反倒是 Giscus 明亮/暗黑模式的切换问题。

因为本博客有自己的切换按钮(见前文),用户访问的时候可能从 localstorage 中取颜色模式偏好,但是目前加载 giscus 是 hugo 在站点生成的时候就将颜色偏好参数写入 html 源码了,因此需要在用户点击按钮切换时,一并切换 giscus 的颜色偏好。参考官方的这个 issue 这一功能并不难实现。然而这样依然有问题,因为 giscus 加载后,用户点击按钮切换颜色模式偏好前,giscus 的颜色偏好是基于我的 hugo 配置文件,而非用户 localstorage 里存储的,结果就是可能用户手动选择了明亮模式,但浏览器设置里有 prefer-color-scheme: dark,所以 giscus 显示黑色背景+白色文字。之前的 issue 里对这个问题没有太好的解法,看到有人 setTimeout 不断循环,但感觉这不太优雅。读了一下官方文档,发现其实 giscus 会在加载完成后向父窗口发送事件,所以其实只要监听这个事件,在 giscus 加载完后再设置 giscus 的颜色偏好即可。相关实现可参考这个 commit

可能切换了之后会有更多评论?但愿吧。

随机分享(230910):Typescript 中 Any 关闭类型检查 & Linux 中的内存占用

2023-09-10 16:05:00

(没有干货,全是湿货…不过至少写一些总比完全没有写强?)

本周遇到的 Bug

遇到了两个前端相关的 bug,排查了很久,不过最后发现都是人的问题而非代码的问题…

  1. CI 坏了还能跑?
    • 现象:某前端项目,其他人参加开发的时候发现 master 分支无法 npm install,但之前这个 repo 一直在正常更新版本,看 CI 日志也一切正常
    • 原因:发现问题是上游某依赖方对已发布的包重新发布,导致文件 hash 变化,npm install 时实际文件 hash 和 lock 中 hash 不一致,所以失败;CI 之所以能跑是因为流水线里加了一层 cache,只要 packages-lock.json 不变就会复用之前的 cache,而恰巧上游重发包之后这个文件一直没变过,所以每次跑 CI 都是拉的已有的 cache,没有实际在流水线里执行 npm install,未能即使暴露故障
    • 解决:重建 packages-lock.json,让 CI 中的 cache 无效
  2. 本地坏了,线上是好的?
    • 现象:某前端项目,例行更新依赖库版本,发布前自测发现某功能测试环境不可用;但相同功能在线上一切正常
    • 原因:拉线上版本回本地排查,发现线上的版本和实际代码的主干版本不一致(!);查阅发布记录,发现线上版本最近发布已经是一年多之前。和开发了解,原来现在的这个前端项目是原来的两个前端项目合并而来的,部署的时候其实要部署两次,但合并后的部署只部署了另一个模块,而没有部署当前模块,所以现在线上跑的实际上还是合并前的版本。
    • 解决:修复代码问题,本地验证通过后发布线上;补充 readme 说明发布时需要两个模块都发布

Typescript 中 Any 关闭了类型检查

某后端项目,因为历史原因代码中有较多 any。最近发现代码中某处接受用户输入的位置有问题,默认值的类型不正确但依然通过了编译。示例如下:

// 不要这样用!
const names: string[] = (req.body as any).names ?? '';
//    ^string[]                           ^any     ^string

注意到这里 nullish coalescing (??)的默认值是个 string 而非 string[]。这段代码感觉上上不应该通过编译,但是因为 (req.body as any)any 类型禁用了类型检查,因此编译时不会再检查缺省值,实际上可以编译通过。而如果不巧后续有函数需要使用 string[] 才有的方法,而 req.body 中的 names 的确为 undefined,就会导致问题。

要解决这个问题,需要提供了更强的类型提示,让 Typescript 的检查可以正常执行。

// 直接标记可能的类型:string[] 或 undefined
const names: string[] = (req.body as any).names as string[] | undefined ?? '';

// 通过一个自定义 type 来实现
type Nullable<T> = T | undefined | null;
const names: string[] = (req.body as any).names as Nullable<string[]> ?? '';

Typescript Playground

Linux 中的内存占用

Linux 进程内存不同计算方法的区分:VSS, RSS, PSS, USS


  ┌────────┐
  │        │
  │        │        ┌────────┐
  │ Unused │        │        │
  │  (A)   │        │ ...    │◄───┐
  │        │        │        │    │Other
  │        │        ├────────┤    │Program's
  │        │        │        │    │Share
  ├────────┤        │ ...    │◄───┘ (D)
  │        │        │        │
  │ Used   │        ├────────┤
  │  (B)   │        │        │
  │        │        │My Share│
  │        │        │   (C)  │
  └────────┘        └────────┘

   Exclusive         Shared

可以把进程的内存占用视作上图。首先程序有自己独占的虚拟内存空间(Exclusive),其中可以分为已经使用了的(B)和属于自己但还未使用的(A)。其次进程还会使用一些共享内存(Shared),例如 so 动态运行库和 mmap 映射。考虑到这些共享内存多个进程都会用到,将其完全计算在某个特定进程名下听起来就不太合理,因此这里可以考虑类似于现实中的"公摊面积",根据实际使用的进程数把这部分内存占用平摊成 N 份,当前进程只计算其中一份(C),剩余的计算在其他进程下(D)。

由此我们可以得到四种不同的计算方法,见下表

指标 含义 组成 说明 图例
VSS 虚拟内存集合(Virtual Set Size) 所有进程地址空间中的所有内存 进程可以访问的虚拟内存空间大小 A+B+C+D
RSS 常驻内存集合(Resident Set Size) 进程当前实际使用的物理内存 实际分配的内存,不需要缺页中断就可以使用 B+C+D
PSS 共享内存集合(Proportional Set Size) 进程当前实际使用的物理内存,按比例分配共享内存 按比例分配共享内存,适用于多个进程共享同一块内存的情况 B+C
USS 独立内存集合(Unique Set Size) 进程独占使用的物理内存 只包含进程独占使用的物理内存,不包括共享库和映射的文件 B

Via:B站视频:用什么指标来衡量我的程序占用了多少内存

其他

最后顺带一提,本博客目前把 RSS 改为了全文输出模式(参考这篇文章实际 commit),希望可以帮到在 RSS 阅读器中阅读本博客的读者。

再次复活(2023)& 换用 Github Action 部署

2023-08-27 19:30:45

又是很久没更新博客了,看了看记录上次更新已经差不多是一年前了。和上次一样,这一年中依然发生了很多:完成了硕士学业,解封前跨过重重关卡回国,入职,打工一年… 真正开始工作的感觉和还是学生时的预期差不多,如果非要比较的话虽然没有大学这么轻松,但是比高中还是好多了的,况且还有钱拿?不过这是后话了,之后再慢慢展开吧,可能以后会有一篇更长的文章来总结这一年(又在挖大概不会填的坑了)。

本次主要的变更是把原来的手动发布流程转换成了 Github Actions,之后直接在 master 分支下提交文件,就会自动触发部署流程,大大减少了阻碍。(对比之前的流程:写文章 - 本地构建 - 复制到部署文件夹 - Git 提交)实际实现起来也很简单,将所有的构建产物(实际发布的静态文件扔到另一个分支 publish),修改 Github Pages 的来源分支,然后写点 Github Action 配置就好了。希望通过让发布的流程更简单,未来可能也会逐渐多写点东西吧。(美好的愿望…)

顺带一提,本次迁移的时候其实尝试过用最新的 Hugo 版本来部署(v0.117.0),但是构建过程中出现了很多模板相关的错误,估计是目前用的 Manic 主题使用到的某些 Hugo 特性已经被 deprecated 了。看了看原始的 Manic repo,最后更新也已经停留在好几年前了。之后要是有空的话可能会试着解决(或者再换一个主题…),不过这次就先暂且继续用旧版本 Hugo 来构建吧。

播放 Lofi Girl 的小脚本

2023-08-27 19:18:26

自测 Lofi 对集中注意力有些帮助,然而如果长时间用 Chrome / Firefox 来播放似乎会导致奇怪的内存溢出问题,原因可能和 Youtube 的播放器默认会缓存已播放的片段有关。换用 MPV 似乎可以解决此问题。于是顺手写了个小脚本,结合 yt-dlp 和 mpv,一键播放 Lofi Girls。使用前请先自行下载 yt-dlpmpv

::set HTTP_PROXY=http://localhost:[some_port]
cd C:\Apps\mpv-x86_64-20230723-git-ca4192e
mpv --no-video  "https://www.youtube.com/watch?v=jfKfPfyJRdk" --script-opts=ytdl_hook-ytdl_path=yt-dlp.exe

另外附上一些常见快捷键(完整见此

9:减小音量
0:增大音量
空格:暂停播放(但是依然会在后台继续缓冲)
M:静音

在 devtool 控制台里爬网站

2023-08-20 19:18:26

最近需要从某个不提供 API 接口的网站爬数据。F12 切换到网络标签页,然后重载页面,可以轻松的观察到其实其实后台是有提供给前端的 API 的。(形如 POST /api/entity/:id)。用 Edge 浏览器自带的 “编辑并重新发送” 功能测试,手动也可以调通。(这是 Edge 浏览器一个超棒的功能,对于偶尔的小调试可以替代 Postman)。理论上到了这一步就可以写点 Python 把数据遍历 ID 把数据爬下来了,不过可能还要处理一些 cookie 之类的麻烦事。与其再写个外部脚本,为什么不在浏览器的控制台里直接写脚本爬呢?

大概框架如下:

const idList = [1,2,3]; 
const results = [];
for (const id of idList) {
    const resp = await fetch(/* */); // F12 网络标签页,右键请求,复制 - 复制为 fetch
    const json = await resp.json()
    results.push({id, json});
    await sleep(1000); // 限制频率
}
console.save(results)

还需要一些辅助函数:

// 可以 await 的 sleep
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));


// 以 JSON 保存 console 中的变量
// src: http://bgrins.github.io/devtools-snippets/#console-save
// via: https://stackoverflow.com/questions/11849562/how-to-save-the-output-of-a-console-logobject-to-a-file
(function(console){

console.save = function(data, filename){

    if(!data) {
        console.error('Console.save: No data')
        return;
    }

    if(!filename) filename = 'console.json'

    if(typeof data === "object"){
        data = JSON.stringify(data, undefined, 4)
    }

    var blob = new Blob([data], {type: 'text/json'}),
        e    = document.createEvent('MouseEvents'),
        a    = document.createElement('a')

    a.download = filename
    a.href = window.URL.createObjectURL(blob)
    a.dataset.downloadurl =  ['text/json', a.download, a.href].join(':')
    e.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null)
    a.dispatchEvent(e)
 }
})(console)

以上,祝使用愉快!