Latest Learning

Stuff I learned recently.

用 tar 的 -(stdin/stdout)配合 | 管道,把目录内容从一端流式送到另一端,全程不落临时文件。比逐文件拷贝更快、更稳,还保留目录结构与权限。

基本形态

# 左侧:tar 打包写到 stdout;右侧:从 stdin 解包到目标目录
tar cf - -C /src dir \
  | tar xf - -C /dst
  • c 创建归档、f - 输出到 -(stdout);x 解包、f --(stdin)读取归档
  • -C dir 先切换目录再操作,避免把绝对/相对路径的前缀带进归档

跨主机/跨进程传送

管道两侧不必是同一进程,常见于 kubectl exec、ssh、curl 等场景:

# 从远端 Pod 拉目录到本地,剥离前两级路径
kubectl exec -n platform "$POD" -- tar cf - /app/config \
  | tar xf - --strip-components=2 -C /home/laborant/config

# 等价写法:左侧把 /app 设为根,归档内只剩 config/ 一级前缀,右侧只需 strip 1
kubectl exec -n platform "$POD" -- tar cf - -C /app config \
  | tar xf - --strip-components=1 -C /home/laborant/config
  • --strip-components=N:去掉每个成员路径最前面的 N 个分量(本例去掉 app/config 两级)
  • exec 不加 -t(TTY),避免换行转换污染二进制流
  • -C 目标目录须先存在,否则报 Cannot open,先 mkdir -p

变体

  • 压缩(网络慢):左侧 tar czf -,右侧 tar xzf -(gzip)
  • 本地备份tar cf - dir | tar xf - 等价 cp -a,但保留更完整属性

注意

  • 两端都需安装 tar(busybox/distroless 容器可能没有)
  • --strip-components 由 GNU tar / bsdtar 提供,macOS / Linux 均有
  • 非 root 解包时文件 owner 变成当前用户(root 下才默认保留)

当 Kubernetes 将 ConfigMap 挂载为 volume 时,pod 内的文件其实是 symlink,直接 kubectl cp 会跳过所有文件导致拷不出内容。用 kubectl exec + tar 管道即可复制真实文件。

问题

# 这样拷贝会失败:目录内全是 symlink,无 .conf 文件落地
kubectl cp platform/${POD}:/app/config/ /home/laborant/config/

warning: skipping symlink: "/home/laborant/config/server.conf" -> "..data/server.conf"
# ...
# kubectl cp 退出码为 0 却什么都没拷出来

原因:kubectl cp 内部用 tar 流式传输,但没传 --dereference。挂载路径下的条目全是 symlink,tar 视其为符号链接而跳过。kubectl cp 即使失败也返回 0,必须 ls 验证结果。

解决方案

在 pod 内部 tar 打包(内核 VFS 透明解析 symlink),再管道给本地 tar 解包:

# 左侧在 pod 内打包并写 stdout,右侧本地解包并剥离前两级路径
kubectl exec -n platform "$POD" -- tar cf - /app/config \
  | tar xf - --strip-components=2 -C /home/laborant/config
  • --strip-components=2:去掉 app/config 两级前缀,只留裸文件名
  • tar xf -:从 stdin 读取归档,-C 指定解包目录
ls /home/laborant/config/
# database.conf  feature-flags.conf  logging.conf  server.conf

通用注意点与变体

  • exec 不要加 -t(TTY):TTY 会对输出做换行转换,污染二进制归档流;无 TTY 的 exec 才是干净的 stdin/stdout 回传
  • -C 目标目录必须先存在tar 不会自动创建目录,会报 Cannot open 之类错误,先 mkdir -p /home/laborant/config
  • 等价写法:左侧 tar cf - -C /app config 时归档成员只有 config/... 一级前缀,右侧只需 --strip-components=1;与”绝对路径 + strip 2”语义等价,任选一种保持一致
  • 压缩变体:网络较慢时左侧 tar czf -(gzip 压缩),右侧 tar xzf - 解压
  • 依赖:Pod 内需装有 tar(busybox/distroless 镜像可能没有,报 exec: tar: not found);本地的 --strip-components 由 GNU tar / bsdtar 提供,macOS / Linux 均有
  • 权限:非 root 解包时文件 owner 会变成当前用户(tar 仅在 root 下默认保留 owner),通常无碍

获取运行中的 pod 名

POD=$(kubectl get pod -n platform \
  -l app=config-service \
  -o jsonpath='{.items[0].metadata.name}')
原子更新机制:两层级联导致 kubectl cp 失效

ConfigMap 以 volume 挂载时,目录结构是两层间接:

/app/config/
├── ..2026_08_19_10_19_29.2041564214/   ← 真实目录,存真实字节
├── ..data -> ..2026_08_19_10_19_29.2041564214   ← 指向真实目录
└── server.conf -> ..data/server.conf    ← 每个文件都是 symlink
  • 权限串首字符 l 表示 symlink(lrwxrwxrwx
  • kubelet 更新 ConfigMap 时写入新时间戳目录,再通过一次原子 rename() 重指 ..data,保证运行中的 pod 立即看到新内容且不出现新旧混合
  • tar 默认不跟随 symlink,kubectl cp 因此全部跳过

kubectl create ingress--rule 语法里,tls 是挂在某条 rule 上的,它的 host 会直接复用该 rule 的 host,命令行层面没有单独指定 tls hosts 的选项

kubectl create ingress snake-ingress -o snake \
  --rule="a.com/=snake:80,tls=snake" \
  --dry-run=client -o yaml

生成结果一定是:

spec:
  rules:
  - host: a.com
    http:
      paths:
      - path: /
        pathType: Exact
        backend:
          service:
            name: snake
            port:
              number: 80
  tls:
  - hosts:
    - a.com
    secretName: snake

rules[].hosttls[].hosts 必然一致,无法让两者不同。

可行的两种办法

  1. 手动改生成的 YAML(最直接):用 --dry-run=client -o yaml 生成后直接编辑 spec.tls[0].hosts,改成你想要的域名(如通配符证书 *.a.com 或额外域名),与 rules[].host 脱钩即可。

  2. 用多条 --rule 聚合多个 host

    kubectl create ingress snake-ingress -o snake \
      --rule="a.com/=snake:80,tls=snake" \
      --rule="b.com/=snake:80,tls=snake" \
      --dry-run=client -o yaml

    这样 tls[0].hosts 会变成 [a.com, b.com],但本质上只是各 rule host 的并集——并不能做到”某条 rule 的 host 不出现在 tls.hosts 里”这种脱钩效果。

如果你的目标是”http 规则匹配 a.com,但证书 SAN 是别的域名(如通配符证书)“,最干净的做法就是方案 1:生成模板后手工改 tls.hostsapply

pi 的 fetch_content / web_searchBlocked internal address ... 198.18.0.0/15:TUN/fake-IP 代理(Surge、Clash、Mihomo)把公网域名解析到保留段,被 SSRF 防护误拦。

~/.pi/web-search.jsonssrf.allowRanges 豁免该网段:

{
  "ssrf": { "allowRanges": ["198.18.0.0/15"] }
}

注意路径是 ~/.pi/web-search.json不是 ~/.pi/agent/。查找顺序:$PI_CODING_AGENT_DIR$XDG_CONFIG_HOME/pi~/.pi

该文件同时是 credential store(放 provider、API key),追加字段时保留现有内容。

免费版私有仓库不支持 GitHub 原生 auto-merge(API 对 allow_auto_merge=true 返回 200 但静默不生效,和 branch protection 一样提示 “Upgrade to GitHub Pro”)。此时 Renovate 托管版 App 默认的 platformAutomerge: true 会失效:PR 上写着 “Automerge: Enabled” 却永远不会合并。

解法:让 Renovate 自己合并 PR,并去掉周期限制(依赖 group:allNonMajor 已把所有 minor/patch 合并成一个 PR,提高频率不会增加噪音):

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended",
    "group:allNonMajor"
  ],
  "rangeStrategy": "bump",
  "dependencyDashboard": false,
  "labels": ["dependencies", "renovate"],
  "automerge": true,
  // 免费版私有仓库用不了 GitHub 原生 auto-merge,由 Renovate 自己合并
  "platformAutomerge": false,
  "packageRules": [
    {
      matchDepTypes: ["peerDependencies"],
      enabled: false,
    },
  ],
  "ignoreDeps": ["node"],
}

排查要点:PR API 返回 autoMergeRequest: null 且 CI 全绿、无 branch protection,却仍不合并,基本就是这个原因。

在 GitHub 页面一键打开对应仓库的 DeepWiki 文档,以及通过 url scheme 唤起本地 Logseq、VSCode 等应用,减少手动拼 URL 的操作。

插件地址:https://chromewebstore.google.com/detail/open-with-for-github/iaicdcencbmacjlfdbgaggfmikfpicig?authuser=0&hl=en

原理

利用 GitHub 仓库页当前的 owner/repo 拼出目标地址,并用 chrome.tabs 或 URL scheme 打开。

实际配置(ghq 管理,本地路径 ~/ghq/github.com/{owner}/{repo}

  • DeepWiki:https://deepwiki.com/{owner}/{repo}
  • Logseq:logseq://graph/MyLifeRecorded?page=github.com/{owner}/{repo}
  • Zed:zed://file/Users/zhaochunqi/ghq/github.com/{owner}/{repo}
  • VSCode:vscode://file/Users/zhaochunqi/ghq/github.com/{owner}/{repo}

示例代码

在 content script 中读取仓库路径,把 github.com/a/b 转为 {owner}/{repo}

// location.href 形如 https://github.com/tauri-apps/tauri
const match = location.href.match(/github\.com\/([^/]+)\/([^/]+)/);
const owner = match[1];
const repo = match[2];

// 打开 DeepWiki
chrome.tabs.create({ url: `https://deepwiki.com/${owner}/${repo}` });

// 打开本地应用(ghq 路径,注意需要 encode 路径中的空格等字符)
location.href =
  `vscode://file/Users/zhaochunqi/ghq/github.com/${owner}/${repo}`;

注意

  • 本地 url scheme 需要系统已注册对应 handler(VSCode 默认注册 vscode://,Zed 注册 zed://),否则会被浏览器忽略。

linux.do 为注册用户提供免费的 DeepLx API 密钥,可以直接填入陪读蛙翻译插件,实现免费高质量的网页翻译。

获取密钥

  1. 注册并登录 linux.do
  2. 访问 https://connect.linux.do/dash/deeplx 复制 DeepLx Api Key

配置陪读蛙

  1. 安装陪读蛙扩展
  2. 点击陪读蛙扩展图标进入选项页面,导航到「API 提供商」
  3. 选择 DeepLX(默认 Base URL 即为 linux.do 的 api.deeplx.org,无需修改)
  4. 在 API Key 处填入从 linux.do 复制的密钥
  5. 回到页面,在功能提供商里选择翻译服务为 DeepLX
  • 填好后点击「测试连接」,成功即完成配置
  • 遵守合理使用规范,避免高频无效请求

一直以为在阿里云买的域名必须完成 ICP 备案才能正常使用,所以有个域名闲置了很久。最近因为需要苹果开发者认证购买了一个域名,发现将 DNS 解析托管到 Cloudflare 即可正常使用,无需备案。

关键认知:备案是针对”在国内使用国内服务器提供服务”的要求。如果业务不面向国内用户、不接入境内服务器,域名本身可以自由使用,不备案也没问题。

操作方式

  1. 在阿里云域名控制台将 DNS 服务器改为 Cloudflare 分配的 NS 地址
  2. 在 Cloudflare 添加域名并配置 DNS 记录
  3. Cloudflare 的代理(橙色云朵)功能也可正常使用

域名解析走 Cloudflare,源站用海外服务器,完全绕开备案流程。

在 iOS Safari 中,顶部“下载/打开 App”大概率来自 Smart App Banner,而“点链接直接跳转 App”通常是 Universal Links

<meta name="apple-itunes-app" content="app-id=544007664">

YouTube 能显示“下载 YouTube”是因为网页声明了与 App Store App 绑定的 apple-itunes-app

如果带上 app-argument,用户点击“打开”可跳转到指定内容页:

<meta
  name="apple-itunes-app"
  content="app-id=544007664, app-argument=https://www.youtube.com/watch?v=dQw4w9WgXcQ"
>

如果 App 未上架 App Store,这一步无法生效,因为 app-id 无法对应有效应用。

Universal Links(网页链接直接唤起 App)

你要额外配置苹果的关联域:网站提供 apple-app-site-association,App 开启 Associated Domains

https://www.youtube.com/.well-known/apple-app-site-association
{
  "applinks": {
    "details": [
      {
        "appIDs": ["TEAMID.com.google.ios.youtube"],
        "components": [
          {
            "/": "/*"
          }
        ]
      }
    ]
  }
}

快速判断

  • Safari 顶部提示“下载/打开 App” → Smart App Banner
  • 点击 YouTube 链接直接切到 App → Universal Links
  • 想让“打开”跳具体内容页:app-argument + App 内处理深链

在 GHCR 里看到很多 untagged,不一定代表清理任务漏删了垃圾镜像。

如果一个 Docker 镜像是 multi-arch 构建,比如同时构建:

platforms: linux/amd64,linux/arm64

那么一个 tag 通常不是直接贴在某个具体平台镜像上,而是贴在一个“目录”上。这个目录的正式名字叫 OCI image index 或 manifest list。

可以把它理解成:

ghcr.io/org/app:sha-xxxxxxx
  -> 一个总入口 / 目录
      -> amd64 机器用的镜像
      -> arm64 机器用的镜像

用户拉镜像时还是用同一个 tag:

docker pull ghcr.io/org/app:sha-xxxxxxx

Docker 会根据本机架构自动挑正确的子镜像。amd64 机器拿 amd64,arm64 机器拿 arm64。

为什么会显示成 1 tagged + 4 untagged

以一个同时构建 linux/amd64linux/arm64,并且没有关掉 buildx 默认 provenance 的镜像为例,一次构建大致会产生:

1 个 tagged image index
2 个 untagged platform image manifest
2 个 untagged provenance attestation manifest

所以 GHCR 页面上看起来就是:

1 tagged + 4 untagged

这里的 2 个 platform manifest 是真正给不同 CPU 架构运行用的镜像。它们没有自己的 tag,因为 tag 贴在上层 image index 上。

另外 2 个 provenance attestation manifest 不是运行镜像,而是构建证明。它们记录“这个镜像是从哪个 repo、哪个 commit、哪个 Dockerfile、哪次 GitHub Actions run 构建出来的,以及用了哪些基础镜像/构建依赖”。常见格式是 in-toto statement,predicate 是 SLSA provenance。

这些 untagged 可以删吗

不要只看 GHCR UI 上写着 untagged 就手动删。

如果这些 untagged 是被某个保留 tag 引用的子 manifest,删掉以后可能会破坏 multi-arch 镜像,或者丢掉 provenance 证明。正确的清理逻辑应该理解 image index 和它下面的子 manifest,而不是把所有 UI 上的 untagged 都当成孤立垃圾。

dataaxiom/ghcr-cleanup-action 这类专门处理 GHCR 清理的工具,会把 multi-arch image、attestation、referrer 这些关系一起考虑。删除旧 tagged image 时,它会连同下面的子 manifest 一起处理;保留新 tagged image 时,被引用的 untagged 子 manifest 也应该留下。

GitHub 能避免这种显示吗

GHCR 目前没有一个开关可以说“不要把被 image index 引用的子 manifest 显示为 untagged”。它是按 registry 里的 manifest/version 展示,所以 UI 会显得比较吵。

能减少数量,但都有代价:

  • 关掉 provenance:每批少两个 attestation,代价是没有构建来源证明。
  • 只构建单平台:少一个平台镜像,代价是不再同时支持 amd64/arm64。
  • 单平台并关掉 provenance:最干净,但功能和供应链信息都缩水。

更实用的判断是:不要追求 GHCR 页面上完全没有 untagged,而是区分“孤立 untagged”和“被保留 tag 引用的 untagged 子 manifest”。前者可以清理,后者是镜像结构的一部分。

市售的食用鸡蛋几乎都是未受精的,无法孵化出小鸡。

原理

  • 食用鸡蛋: 养殖场只养母鸡或严格隔离公鸡,母鸡无需交配就能正常产蛋,这些蛋只是卵细胞,没有胚胎
  • 受精蛋: 需要母鸡和公鸡交配后产下,在 37.5°C 和适当湿度下孵化约 21 天才能出壳

如何区分

  • 照蛋: 用强光照射,受精蛋能看到血丝或胚胎痕迹
  • 打开看: 受精蛋的蛋黄上有小白点(胚盘),但生鲜时很难分辨

超市买的鸡蛋放多久都孵不出小鸡,只会变质。

GitHub token 校验不要依赖固定长度,尤其不要写类似 ghs_[A-Za-z0-9]{36} 的正则。

GitHub 官方文档列出的 token 前缀包括:

  • ghp_: classic personal access token
  • github_pat_: fine-grained personal access token
  • gho_: OAuth access token
  • ghu_: GitHub App user access token
  • ghs_: GitHub App installation access token
  • ghr_: GitHub App refresh token

2026-04-27 起,GitHub App installation token 会逐步切到 stateless 格式,ghs_ token 会变成类似 ghs_APPID_JWT 的结构,长度约 520 字符且会变化。JWT 部分也不应该由客户端解析或校验语义。

如果业务只是想拦住明显错误的输入,可以只做轻量格式检查:

const githubTokenRe = /^(?:(?:ghp|gho|ghu|ghs|ghr)_|github_pat_)[A-Za-z0-9_.-]+$/;

这个校验只确认是 GitHub 已知 token 前缀,并允许 JWT 常见的 .-。真正有效性仍然应该交给 GitHub API 返回 401 Bad credentials 之类的结果判断。

更稳的原则:

  • token 当作 opaque string,不解析内部内容
  • 不限制固定长度
  • 存储字段至少能放下 520 字符以上
  • Actions 里优先使用内置 GITHUB_TOKEN,跨 repo 或特殊权限才使用 PAT 或 GitHub App

参考:

  1. GitHub Docs: GitHub’s token formats
  2. GitHub Changelog: Notice about upcoming new format for GitHub App installation tokens

Workflowy 早期的 inline editing 不是直接用 contenteditable,而是把一个透明的 textarea 精确覆盖在当前 hover 的文本上,点击后再切换可见层。

实现思路:

  1. 页面上正常渲染文本内容。
  2. 鼠标 hover 某一项时,把一个 opacity: 0textarea 移动到这段文本上方。
  3. 点击时 focus textarea,把 textarea 设为 opacity: 1,同时把底层文本设为 opacity: 0
  4. 输入时同步 textarea.value 到底层渲染节点。
  5. blur 后恢复底层文本显示,隐藏编辑框。

这样做的好处是输入、选区、键盘行为都交给原生 textarea 处理,同时显示层仍可以自己控制高亮、链接、tag 等富文本效果。

<!doctype html>
<meta charset="utf-8" />
<style>
  .item {
    position: relative;
    padding: 4px 8px;
    font: 16px/1.5 system-ui, sans-serif;
    white-space: pre-wrap;
  }

  #editor {
    position: absolute;
    z-index: 10;
    opacity: 0;
    resize: none;
    overflow: hidden;
    border: 0;
    padding: 4px 8px;
    margin: 0;
    font: 16px/1.5 system-ui, sans-serif;
    background: transparent;
    outline: none;
  }
</style>

<div class="item">Buy milk #todo</div>
<div class="item">Read https://example.com</div>
<textarea id="editor"></textarea>

<script>
  const editor = document.querySelector('#editor');
  let currentItem = null;

  document.querySelectorAll('.item').forEach((item) => {
    item.addEventListener('mouseenter', () => {
      if (document.activeElement === editor) return;
      moveEditorOver(item);
    });
  });

  function moveEditorOver(item) {
    currentItem = item;
    const rect = item.getBoundingClientRect();

    editor.value = item.textContent;
    editor.style.left = `${rect.left + window.scrollX}px`;
    editor.style.top = `${rect.top + window.scrollY}px`;
    editor.style.width = `${rect.width}px`;
    editor.style.height = `${rect.height}px`;
    editor.style.opacity = 0;
  }

  editor.addEventListener('mousedown', () => {
    if (!currentItem) return;
    currentItem.style.opacity = 0;
    editor.style.opacity = 1;
  });

  editor.addEventListener('input', () => {
    if (!currentItem) return;
    currentItem.textContent = editor.value;
  });

  editor.addEventListener('blur', () => {
    if (!currentItem) return;
    currentItem.style.opacity = 1;
    editor.style.opacity = 0;
  });
</script>

来源:How does workflowy implemented inline editing?

想让 AI 在不熟悉的领域里产出更好,先学那个领域的 glossary。

很多 vibecoding 前端看起来差,不一定是模型不会写,而是 prompt 里只有「菜单」「按钮」「页面」这种粗粒度词。换成更准确的 UI component 名称,模型会更容易命中它内部已经学过的 pattern。

例如不要只说:

做一个有菜单和按钮的设置页。

先补一点 domain knowledge,再说:

做一个 settings surface:
- 左侧用 sidebar navigation
- 顶部用 toolbar,包含 segmented control 和 search field
- 主区域用 form section、field group、select、switch、slider
- 危险操作放到 destructive action zone
- 保存状态用 inline validation 和 toast feedback

这不是堆术语,而是在给 LLM 更精确的索引词。对任何陌生领域都一样:先问 AI 或文档要一份 glossary,掌握关键概念和对象名,再开始让它做事。磨刀不误砍柴工。

可以先看这些网站补词汇:

  1. The Component Gallery:查 UI component 的标准名称、别名、定义和真实 design system 示例。
  2. Domain Maps:用 domain map 先建立陌生领域的概念坐标,再把关键术语喂给 LLM。

docker-compose.ymltty: truestdin_open: true 这两个配置项分别对应 docker run-t-i 参数。

tty: true

分配虚拟终端(Pseudo-TTY),让程序输出像在普通命令行中运行,保留颜色和交互式输出。

stdin_open: true

保持标准输入开启,即使没有 attach 到容器也能接收指令。

适用场景

Minecraft 服务器等需要交互式控制台的服务。

为什么 Minecraft 必须用它们

Minecraft 服务器有交互式控制台,可以:

  • 手动输入命令:/op yourname 给自己管理员权限,或 /stop 安全关闭服务器
  • 使用 docker attach 进入游戏控制台直接输入指令

如果没开这两个选项,docker attach 只能看输出,无法输入命令。

services:
  mc:
    image: itzg/minecraft-server
    tty: true
    stdin_open: true

总结:tty 负责输出,stdin_open 负责输入。

公司网络能审计 HTTPS 访问记录,并非”破解了加密”,而是利用了 TLS 握手阶段明文传输的 SNI(Server Name Indication)

原理

在 HTTPS(即 TLS)连接中,代理/防火墙无需解密流量,也无需建立 CONNECT 隧道,只需旁路观察 TLS 握手的第一个 ClientHello 报文。

TLS 握手流程(简化)

  1. TCP 三次握手
  2. 客户端发送 ClientHello(明文)
  3. 服务器响应 ServerHello
  4. 后续加密通信

ClientHello 中的 SNI

ClientHello未加密的明文数据,其中包含扩展字段。SNI 格式如下:

Extension: server_name
Hostname: example.com

代理/防火墙只需解析这个明文握手包,即可获取目标域名,无需:

  • 解密 TLS 流量
  • 建立了 CONNECT 隧道(传统 HTTP 代理模式)

SNI 是 TLS 扩展,用于解决单 IP 多域名场景。客户端必须在 ClientHello明文告诉服务器想访问的域名,否则服务器无法选择正确证书。

审计边界

  • ✅ 能看到:目标域名(SNI)、目标 IP、连接时间、流量大小、连接时长
  • ❌ 不能看到:URL 路径、查询参数、HTTP Header、页面内容、POST 数据

HTTPS(TLS)保护的只是 HTTP 请求内容(路径、Header、Body),但不会隐藏目标域名和 IP 等元数据。

防御方式

  • ECH(Encrypted Client Hello):TLS 1.3 扩展,使用 DNS 公钥加密 SNI
  • DoH/DoT:防止 DNS 查询泄露,但无法阻止 SNI 泄露
更彻底的审计:HTTPS 中间人解密(MITM)

企业可在终端安装根证书,代理动态生成假证书解密流量。但已不属于纯 CONNECT 透传。

根据 2026 年最新实测数据,简单有效的 email 地址防爬虫方法:

纯文本 email

方法阻止率
无保护0%
HTML Entities95%
HTML 注释98%
HTML SVG100%
CSS display: none100%
JS 拼接100%
JS 转换 (自定义函数)100%
JS AES 加密100%

可点击 mailto 链接

方法阻止率
无保护0%
HTML Entities100%
URL 编码96%
HTTP 重定向100%
HTML SVG100%
JS 拼接100%
JS 转换100%
JS AES 加密100%

推荐方案

最简单且有效:CSS display: none + 变化诱饵标签

<div class="email">ad@<span>email.</span>spencermortensen.<span>example.</span>com</div>
div.email > span:nth-child(2) { display: none; }

无 JS 依赖,完全无障碍可用。

推荐方案:JS 转换(自定义函数)

<span id="email">zibby example com</span>
const map = { zibby: 'hello', example: 'gmail', com: 'com' };
// 自定义转换逻辑

原理:HTML 源码只有乱码,需要在浏览器端用 JS 转换才能得到真实 email。

最强方案:JS AES 加密(需 HTTPS)

<span class="email">Kreuz2xa6xB8Fpjaa0lFgACNLO6n_Auu1CGjcG8z_Ec</span>

使用浏览器内置 SubtleCrypto 进行 AES-256 加密。

不推荐(破坏可用性)

  • 符号替换 (ag AT email DOT com) - 用户需手动还原
  • 图片 - 无法复制、屏幕阅读器无法读取
  • CSS content - 文本不可选中
  • CSS 文字方向反转 - 复制后是反的

关键发现:大多数爬虫很简单,即使最基础的混淆技术也能阻止 95% 以上的爬虫。建议组合使用多种技术。

来源: Email obfuscation: What works in 2026?

Docker Compose 默认会给卷名加上项目前缀(目录名),使用 name 属性可自定义卷名。

services:
  db:
    image: postgres
    volumes:
      - my_custom_volume:/var/lib/postgresql/data

volumes:
  my_custom_volume:
    name: "production_db_volume"

运行 docker compose up 后,docker volume ls 会显示 production_db_volume,而非 my-app_my_custom_volume

适用场景:

  • 多项目共享卷
  • 固定名字便于脚本编写和迁移

Docker Compose 中的 healthcheck 配置会覆盖 Dockerfile 中的 HEALTHCHECK 指令。

# docker-compose.yml
services:
  web:
    image: myapp
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

优先级:Dockerfile < Docker Compose。

为什么 Compose 优先?

遵循”运行时配置优于镜像构建配置”原则。Compose 覆盖 Dockerfile 的优势:

  • 灵活性:不同环境(开发/测试/生产)可配置不同的检查频率或超时时间
  • 依赖管理:配合 depends_oncondition: service_healthy 控制容器启动顺序
  • 集中管理:在 Compose 文件中一目了然,无需翻阅各镜像源码