<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>CYK</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://cyk.pink/</id>
  <link href="https://cyk.pink/" rel="alternate"/>
  <link href="https://cyk.pink/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, CYK</rights>
  <subtitle>记录学习和日常生活</subtitle>
  <title>CYK's Blog</title>
  <updated>2026-10-27T07:20:00.000Z</updated>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="运维笔记" scheme="https://cyk.pink/categories/%E8%BF%90%E7%BB%B4%E7%AC%94%E8%AE%B0/"/>
    <category term="Docker" scheme="https://cyk.pink/tags/Docker/"/>
    <category term="Twikoo" scheme="https://cyk.pink/tags/Twikoo/"/>
    <category term="DNS" scheme="https://cyk.pink/tags/DNS/"/>
    <category term="nginx" scheme="https://cyk.pink/tags/nginx/"/>
    <content>
      <![CDATA[<h2 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</h2><p>博客的评论区一直在转圈，一直以为是「没人评论所以空的」。</p><p>直到有一天为了查别的问题去调评论接口，发现请求根本没回来：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">curl -X POST https://xxx.vercel.app/ → 超时</span><br></pre></td></tr></table></figure><h2 id="第一步：先看-DNS，别急着改代码"><a href="#第一步：先看-DNS，别急着改代码" class="headerlink" title="第一步：先看 DNS，别急着改代码"></a>第一步：先看 DNS，别急着改代码</h2><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">nslookup xxx.vercel.app</span><br></pre></td></tr></table></figure><p>结果：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">Addresses:  2a03:2880:f12c:183:face:b00c:0:25de</span><br><span class="line">            31.13.85.2</span><br></pre></td></tr></table></figure><p><code>face:b00c</code> 是 Facebook 的 IPv6 特征段，<code>31.13.x</code> 也是 Facebook 的地址块。再查一个对照组：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">nslookup vercel.com        → 64.239.123.1   ✅ 正常</span><br><span class="line">nslookup test.vercel.app   → 208.43.170.231 + face:b00c  ❌ 同样被劫持</span><br></pre></td></tr></table></figure><p>结论：**<code>*.vercel.app</code> 整段被 DNS 污染**，解析结果全被指到 Facebook 的 IP 上，所以永远连不上。Vercel 本身没问题，<code>vercel.com</code> 是好的。</p><p>这也解释了为什么有些人抱怨「Vercel 上的服务国内打不开」——不是平台的问题，是域名被挡住了。</p><h2 id="第二步：自定义域名能用，但有版本坑"><a href="#第二步：自定义域名能用，但有版本坑" class="headerlink" title="第二步：自定义域名能用，但有版本坑"></a>第二步：自定义域名能用，但有版本坑</h2><p>我早先给评论服务绑过一个自定义域（<code>twikoo.xxx.com</code>，CNAME 到 Vercel 的 DNS），这个域名没被污染，200 正常响应。</p><p>但调接口返回：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span><span class="attr">&quot;code&quot;</span><span class="punctuation">:</span><span class="number">1001</span><span class="punctuation">,</span><span class="attr">&quot;message&quot;</span><span class="punctuation">:</span><span class="string">&quot;请更新 Twikoo 云函数至最新版本&quot;</span><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>按字面意思，应该去升级后端。于是我把后端镜像从头到尾换了几个版本——<strong>升到最新还是 1001</strong>。</p><p>回头看源码才发现，这句文案是误导的：</p><figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// @twikoojs/common</span></span><br><span class="line"><span class="attr">EVENT_NOT_EXIST</span>: <span class="number">1001</span>,</span><br></pre></td></tr></table></figure><p><strong>1001 的真实含义是「事件不存在」。</strong></p><p>这就对上了：前端加载的是 1.6.x，发的是 <code>GET_COMMENTS</code> 事件；后端早就是 2.x，事件名变了，直接不认。而提示文案还留在旧版本那句话上。</p><p>验证方法：换成不涉及事件名的接口试试：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">GET_CONFIG            → 正常返回配置</span><br><span class="line">GET_RECENT_COMMENTS   → 正常返回评论数据</span><br><span class="line">GET_COMMENTS          → 1001</span><br></pre></td></tr></table></figure><p>配置能读、数据能读，只有那一个事件不通——<strong>根本不是版本太旧，是前后端事件名对不上</strong>。</p><h2 id="第三步：干脆搬回自己机器"><a href="#第三步：干脆搬回自己机器" class="headerlink" title="第三步：干脆搬回自己机器"></a>第三步：干脆搬回自己机器</h2><p>既然域名被污染、版本又纠缠，索性不再依赖 Vercel。自己的服务器上跑一个容器就行：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docker run -d --name twikoo \</span><br><span class="line">  --restart unless-stopped \</span><br><span class="line">  --env-file /root/twikoo.env \</span><br><span class="line">  -p 127.0.0.1:8082:8080 \</span><br><span class="line">  -v /mydata/twikoo/data:/app/data \</span><br><span class="line">  imaegoo/twikoo:latest</span><br></pre></td></tr></table></figure><p>几个要点：</p><ul><li>**只监听 <code>127.0.0.1</code>**，公网访问一律走 nginx 反代</li><li>数据库连接串放在 <code>--env-file</code> 里，文件权限 <code>600</code>，不要写在命令行（会进 shell history）</li><li>数据库还是用原来的 MongoDB Atlas，<strong>历史评论不用迁</strong>，连上就能读</li></ul><p><code>/root/twikoo.env</code>：</p><figure class="highlight ini"><table><tr><td class="code"><pre><span class="line"><span class="attr">MONGODB_URI</span>=mongodb+srv://用户:密码@集群/?retryWrites=<span class="literal">true</span>&amp;w=majority</span><br><span class="line"><span class="attr">TWIKOO_THROTTLE</span>=<span class="number">1000</span></span><br></pre></td></tr></table></figure><h2 id="第四步：nginx-反代到一个路径"><a href="#第四步：nginx-反代到一个路径" class="headerlink" title="第四步：nginx 反代到一个路径"></a>第四步：nginx 反代到一个路径</h2><p>不想再折腾子域名和证书，直接挂在主域名的路径下：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="section">location</span><span class="regexp"> ^~</span> /api/twikoo &#123;</span><br><span class="line">    <span class="attribute">rewrite</span><span class="regexp"> ^/api/twikoo/?(.*)$</span> /<span class="variable">$1</span> <span class="literal">break</span>;</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://127.0.0.1:8082;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Forwarded-Proto <span class="variable">$scheme</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>rewrite ... break</code> 那行是关键：容器的接口在根路径 <code>/</code> 上，请求进来是 <code>/api/twikoo</code>，必须把前缀削掉再转发，否则容器收到 <code>/api/twikoo</code> 会 404。</p><p>博客配置改成：</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">twikoo:</span></span><br><span class="line">  <span class="attr">envId:</span> <span class="string">https://你的域名/api/twikoo</span></span><br></pre></td></tr></table></figure><p>好处是：<strong>不用动 DNS、不用单独申请证书</strong>，主域名有什么配置它就用什么。</p><h2 id="第五步：前端也得是-2-x"><a href="#第五步：前端也得是-2-x" class="headerlink" title="第五步：前端也得是 2.x"></a>第五步：前端也得是 2.x</h2><p>后端换好了，前端还停在 1.6.x，照样不通。但直接改版本号会撞墙：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">https://cdn.staticfile.org/twikoo/2.0.17/twikoo.all.min.js  → 404</span><br><span class="line">https://cdn.staticfile.org/twikoo/1.6.31/twikoo.all.min.js  → 200</span><br></pre></td></tr></table></figure><p>国内 CDN 只同步到 1.6.31，jsdelivr 这边又连不上。绕开的办法是从国内 npm 镜像把包下下来，<strong>放到自己站点里</strong>：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">curl -o <span class="built_in">source</span>/js/twikoo.all.min.js \</span><br><span class="line">  <span class="string">&quot;https://registry.npmmirror.com/twikoo/2.0.17/files/dist/twikoo.all.min.js&quot;</span></span><br></pre></td></tr></table></figure><p>然后在主题配置里覆盖这个脚本的地址（Butterfly 支持逐个插件指定 URL）：</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">CDN:</span></span><br><span class="line">  <span class="attr">option:</span></span><br><span class="line">    <span class="attr">twikoo:</span> <span class="string">/js/twikoo.all.min.js</span></span><br></pre></td></tr></table></figure><p>顺带的好处：评论系统不再依赖任何外部 CDN，外部 CDN 出问题、域名被墙，跟我没关系了。</p><h2 id="结果"><a href="#结果" class="headerlink" title="结果"></a>结果</h2><table><thead><tr><th>项目</th><th>之前</th><th>现在</th></tr></thead><tbody><tr><td>评论接口</td><td><code>*.vercel.app</code>，国内不通</td><td><code>自己的域名/api/twikoo</code></td></tr><tr><td>前端脚本</td><td>第三方 CDN 1.6.x</td><td>本站托管 2.0.x</td></tr><tr><td>数据</td><td>Atlas</td><td>Atlas（不变，直接沿用）</td></tr><tr><td>部署平台</td><td>Vercel</td><td>自己的 Docker</td></tr></tbody></table><p>历史评论一条没丢——数据一直在数据库里，之前只是「接口调不通」，不是「数据没了」。</p><h2 id="复盘"><a href="#复盘" class="headerlink" title="复盘"></a>复盘</h2><p>这次真正花时间的不是部署，是<strong>被错误提示带偏</strong>。「请更新云函数」这句话让我在后端版本上兜了好几圈，而真实原因（事件名变了）藏在源码的一个常量定义里。</p><p>遇到报错，尤其是第三方封装过的报错，<strong>先去查错误码的真实定义</strong>，别信文案。</p>]]>
    </content>
    <id>https://cyk.pink/posts/5e83b06a.html</id>
    <link href="https://cyk.pink/posts/5e83b06a.html"/>
    <published>2026-10-27T07:20:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</h2><p>博客的评论区一直在转圈，一直以为是「没人评论所以空的」。</p>
<p>直到有一天为了查别的问题去调评论接口，发现请求根本没回来：</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">curl -X POST https://xxx.vercel.app/ → 超时</span><br></pre></td></tr></table></figure>

<h2 id="第一步：先看-DNS，别急着改代码"><a href="#第一步：先看-DNS，别急着改代码" class="headerlink" title="第一步：先看 DNS，别急着改代码"></a>第一步：先看 DNS，别急着改代码</h2><figure class="highlight plaintext"><table><tr><td]]>
    </summary>
    <title>评论区加载不出来？把 Twikoo 从 Vercel 搬回自己服务器</title>
    <updated>2026-10-27T07:20:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="运维笔记" scheme="https://cyk.pink/categories/%E8%BF%90%E7%BB%B4%E7%AC%94%E8%AE%B0/"/>
    <category term="性能优化" scheme="https://cyk.pink/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    <category term="nginx" scheme="https://cyk.pink/tags/nginx/"/>
    <category term="HTTP2" scheme="https://cyk.pink/tags/HTTP2/"/>
    <category term="图床" scheme="https://cyk.pink/tags/%E5%9B%BE%E5%BA%8A/"/>
    <content>
      <![CDATA[<h2 id="先量化，再优化"><a href="#先量化，再优化" class="headerlink" title="先量化，再优化"></a>先量化，再优化</h2><p>重装之后顺手给站点做了一次体检，四个数字最扎眼：</p><table><thead><tr><th>指标</th><th>数值</th></tr></thead><tbody><tr><td>HTTPS</td><td>只有 HTTP&#x2F;1.1</td></tr><tr><td>gzip</td><td>只压 HTML，CSS&#x2F;JS 全裸奔</td></tr><tr><td>图片缓存头</td><td>没有</td></tr><tr><td>图床里没人引用的死图</td><td>40 个，占 6.5MB &#x2F; 8.9MB</td></tr></tbody></table><p>前三个都是 nginx 一行配置的事，最后一个是长期积累的垃圾。</p><h2 id="HTTP-2：一个连接跑完所有资源"><a href="#HTTP-2：一个连接跑完所有资源" class="headerlink" title="HTTP&#x2F;2：一个连接跑完所有资源"></a>HTTP&#x2F;2：一个连接跑完所有资源</h2><p>HTTP&#x2F;1.1 的问题是「一个连接同时只能处理一个请求」，浏览器只能靠开 6 个并发连接硬堆。页面有几十个资源的时候，排队是实打实的延迟。</p><p>开启只需要在 listen 后面加一个词：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="attribute">listen</span> <span class="number">443</span> ssl http2;</span><br></pre></td></tr></table></figure><p>验证别只看配置，要实际协商：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">echo</span> | openssl s_client -connect 127.0.0.1:443 -servername 你的域名 -alpn h2 2&gt;/dev/null \</span><br><span class="line">  | grep -i <span class="string">&quot;ALPN protocol&quot;</span></span><br></pre></td></tr></table></figure><p>看到 <code>ALPN protocol: h2</code> 才算真的开了。</p><h2 id="gzip：默认只压-HTML"><a href="#gzip：默认只压-HTML" class="headerlink" title="gzip：默认只压 HTML"></a>gzip：默认只压 HTML</h2><p>这是最容易忽略的一项。nginx 的 <code>gzip on;</code> 打开之后，<code>gzip_types</code> 的**默认值只有 <code>text/html</code>**——CSS、JS、JSON、XML 一个都不压。</p><p>Ubuntu 的默认配置里这一行是注释掉的，很多人以为开了 gzip 就万事大吉：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="attribute">gzip</span> <span class="literal">on</span>;</span><br><span class="line"><span class="attribute">gzip_vary</span> <span class="literal">on</span>;</span><br><span class="line"><span class="attribute">gzip_comp_level</span> <span class="number">6</span>;</span><br><span class="line"><span class="attribute">gzip_min_length</span> <span class="number">1024</span>;</span><br><span class="line"><span class="attribute">gzip_types</span> text/plain text/css text/xml application/json application/javascript</span><br><span class="line">           application/xml application/xml+rss image/svg+xml;</span><br></pre></td></tr></table></figure><p>验证：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">curl -s -o /dev/null -H <span class="string">&quot;Accept-Encoding: gzip&quot;</span> \</span><br><span class="line">  -w <span class="string">&quot;%&#123;size_download&#125; bytes\n&quot;</span> https://你的域名/css/xxx.css</span><br><span class="line">curl -sI -H <span class="string">&quot;Accept-Encoding: gzip&quot;</span> https://你的域名/css/xxx.css | grep -i content-encoding</span><br></pre></td></tr></table></figure><p>我这个站上的 CSS 压完大概剩三成。</p><h2 id="缓存头：让重复访问变成零请求"><a href="#缓存头：让重复访问变成零请求" class="headerlink" title="缓存头：让重复访问变成零请求"></a>缓存头：让重复访问变成零请求</h2><p>图片走 nginx 反代到对象存储，响应里只有 ETag 和 Last-Modified，<strong>没有 Cache-Control</strong>——意味着浏览器每次都要回源确认一遍。图多的页面，读者翻两页就能感觉到。</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="section">location</span><span class="regexp"> ^~</span> /cykblog/ &#123;</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://127.0.0.1:9000;</span><br><span class="line">    <span class="attribute">add_header</span> Cache-Control <span class="string">&quot;public, max-age=2592000&quot;</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>静态资源同理：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="section">location</span> <span class="regexp">~* \.(?:css|js|jpg|jpeg|png|gif|webp|svg|ico|woff2?|ttf|eot)$</span> &#123;</span><br><span class="line">    <span class="attribute">expires</span> <span class="number">30d</span>;</span><br><span class="line">    <span class="attribute">add_header</span> Cache-Control <span class="string">&quot;public&quot;</span>;</span><br><span class="line">    <span class="attribute">try_files</span> <span class="variable">$uri</span> =<span class="number">404</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注意一个 add_header 的坑：<strong>location 里一旦写了 add_header，server 层的 add_header 就不再继承</strong>。所以安全响应头要在每个自己写了 header 的 location 里重复一遍，否则 <code>/cykblog/</code> 会漏掉。</p><h2 id="server-tokens-和安全头"><a href="#server-tokens-和安全头" class="headerlink" title="server_tokens 和安全头"></a>server_tokens 和安全头</h2><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="attribute">server_tokens</span> <span class="literal">off</span>;</span><br><span class="line"><span class="attribute">add_header</span> X-Content-Type-Options <span class="string">&quot;nosniff&quot;</span> always;</span><br><span class="line"><span class="attribute">add_header</span> X-Frame-Options <span class="string">&quot;SAMEORIGIN&quot;</span> always;</span><br><span class="line"><span class="attribute">add_header</span> Referrer-Policy <span class="string">&quot;strict-origin-when-cross-origin&quot;</span> always;</span><br><span class="line"><span class="attribute">add_header</span> Strict-Transport-Security <span class="string">&quot;max-age=2592000&quot;</span> always;</span><br></pre></td></tr></table></figure><p>关掉版本号不是为了「安全多少」，是为了别主动告诉扫描器「我是 nginx 1.24.0，你可以去查这个版本的 CVE」。</p><p>HSTS 我一开始只给了 30 天，没设 <code>preload</code>。这个头一旦下发，浏览器在有效期内会强制走 HTTPS——证书出问题时网站会直接打不开，所以先短后长比较稳。</p><h2 id="第三方-CDN-的两个教训"><a href="#第三方-CDN-的两个教训" class="headerlink" title="第三方 CDN 的两个教训"></a>第三方 CDN 的两个教训</h2><p><strong>第一个：图标字体加载失败。</strong> 页面里引用了一个国内 CDN 的 iconfont，报 <code>net::ERR_CONNECTION_CLOSED</code>。在服务器上直接抓那个文件是 200，说明 CDN 本身没问题，是我本地到它的链路被掐了。</p><p>解决办法不是换 CDN，是把字体<strong>搬到自己的域名下</strong>：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">curl -o iconfont.css <span class="string">&quot;https://原CDN/iconfont.css&quot;</span></span><br><span class="line">curl -o iconfont.woff2 <span class="string">&quot;https://原CDN/iconfont.woff2&quot;</span></span><br></pre></td></tr></table></figure><p>CSS 里用的是相对路径（<code>url(&#39;iconfont.woff2&#39;)</code>），放同一个目录就能自动找到，不用改。</p><p><strong>第二个：统计脚本报 ERR_BLOCKED_BY_ORB。</strong> 一个免费访问量统计服务，请求失败后还会挂两分钟才超时，直接把首屏拖慢。关掉，换成 Cloudflare 的 Web Analytics（无 cookie、脚本小、国内可达）。</p><h2 id="图床清理：40-个死图占-73-空间"><a href="#图床清理：40-个死图占-73-空间" class="headerlink" title="图床清理：40 个死图占 73% 空间"></a>图床清理：40 个死图占 73% 空间</h2><p>把桶里所有对象和「页面里实际引用到的」做了一次差集，结果很难看：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">对象总数 80，被引用 51，无人引用 40，占用 6.5MB / 8.9MB</span><br></pre></td></tr></table></figure><p>最大的一个是一张 2560×1440 的 PNG 壁纸，<strong>4MB</strong>——而且同一张图我早就转过 webp（104KB），只是忘了删 PNG，也没在用。</p><p>清理时踩了一个坑：<strong>用文件名直接 grep HTML 会漏</strong>。中文名和带反引号的文件名在 HTML 里是 URL 编码的：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">磁盘：`scroll-view`滑动布局.webp</span><br><span class="line">HTML：%60scroll-view%60%E6%BB%91%E5%8A%A8%E5%B8%83%E5%B1%80.webp</span><br></pre></td></tr></table></figure><p>第一次按文件名匹配，把它们当成「无人引用」删了，结果 12 张正在用的图直接 404。还好删之前打了包，全部恢复。</p><p>正确的做法是<strong>先解码再比对</strong>，而且要同时扫 html &#x2F; css &#x2F; js 三类文件：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">for</span> m <span class="keyword">in</span> re.findall(<span class="string">r&#x27;cykblog/([^&quot;\&#x27;\s)&lt;&gt;?#]+)&#x27;</span>, txt):</span><br><span class="line">    refs.add(urllib.parse.unquote(m))</span><br></pre></td></tr></table></figure><p>改完之后桶从 8.9MB 降到 3.0MB，引用完整性校验为 0 缺失。</p><h2 id="上传链路：把规范写进工具"><a href="#上传链路：把规范写进工具" class="headerlink" title="上传链路：把规范写进工具"></a>上传链路：把规范写进工具</h2><p>以前传图是「拖进去就完事」，于是文件名长这样：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">image-20240729225711008.png</span><br><span class="line">wallhaven-6d61g4_2560x1440.png</span><br><span class="line">`swiper`纵向轮播1.webp</span><br></pre></td></tr></table></figure><p>现在用 PicGo 传，把规则配死：</p><ul><li><strong>路径</strong>：<code>posts/&#123;年&#125;/&#123;月&#125;/</code>（对象存储里的目录）</li><li><strong>文件名</strong>：<code>&#123;y&#125;&#123;m&#125;&#123;d&#125;-&#123;rand:6&#125;</code>，例如 <code>20261027-a3f9c2.webp</code></li></ul><p>有个细节值得记一下：PicGo 的 MinIO 插件里，<code>pathFormat</code> 只支持 <code>&#123;bucket&#125;</code> 一个占位符，**不支持 <code>&#123;年&#125;/&#123;月&#125;</code>**。如果照着别的插件文档填，最后会生成一个字面量路径 <code>posts/&#123;年&#125;/&#123;月&#125;/</code>，对象存的路径和返回的 URL 对不上，图片必然 404。</p><h2 id="效果"><a href="#效果" class="headerlink" title="效果"></a>效果</h2><p>改动都不大，但叠加起来：</p><ul><li>资源请求从「串行排队」变成「一个连接并发」</li><li>CSS&#x2F;JS 传输量降到三成左右</li><li>重复访问的图片基本不再回源</li><li>图床体积减少 66%</li><li>首屏少了一个会挂两分钟的第三方请求</li></ul><p>都是教科书里的东西，只是以前觉得「博客而已，无所谓」。</p>]]>
    </content>
    <id>https://cyk.pink/posts/9a1c47d2.html</id>
    <link href="https://cyk.pink/posts/9a1c47d2.html"/>
    <published>2026-10-27T03:50:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="先量化，再优化"><a href="#先量化，再优化" class="headerlink" title="先量化，再优化"></a>先量化，再优化</h2><p>重装之后顺手给站点做了一次体检，四个数字最扎眼：</p>
<table>
<thead>
<tr>
<th>指标</th>
<th>数值</th>
</tr>
</thead>
<tbody><tr>
<td>HTTPS</td>
<td>只有 HTTP&#x2F;1.1</td>
</tr>
<tr>
<td>gzip</td>
<td>只压 HTML，CSS&#x2F;JS 全裸奔</td>
</tr>
<tr>
<td>图片缓存头</td>
<td>没有</td>
</tr>
<tr>
<td>图床里没人引用的死图</td>
<td>40 个，占 6.5MB &#x2F; 8.9MB</td>
</tr>
</tbody></table>
<p>前三个都是 nginx 一行配置的事，最后一个是长期积累的垃圾。</p>
<h2 id="HTTP-2：一个连接跑完所有资源"><a]]>
    </summary>
    <title>静态博客的传输层优化：HTTP/2、gzip 和缓存头</title>
    <updated>2026-10-27T03:50:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="运维笔记" scheme="https://cyk.pink/categories/%E8%BF%90%E7%BB%B4%E7%AC%94%E8%AE%B0/"/>
    <category term="安全" scheme="https://cyk.pink/tags/%E5%AE%89%E5%85%A8/"/>
    <category term="运维" scheme="https://cyk.pink/tags/%E8%BF%90%E7%BB%B4/"/>
    <category term="nginx" scheme="https://cyk.pink/tags/nginx/"/>
    <category term="SSH" scheme="https://cyk.pink/tags/SSH/"/>
    <category term="fail2ban" scheme="https://cyk.pink/tags/fail2ban/"/>
    <content>
      <![CDATA[<h2 id="重装之后的第一件事不是装软件"><a href="#重装之后的第一件事不是装软件" class="headerlink" title="重装之后的第一件事不是装软件"></a>重装之后的第一件事不是装软件</h2><p>系统重装完，<code>ssh root@ip</code> 一通，密码进去——<strong>这一步就已经错了</strong>。</p><p>上一台机器出事，本质就是「密码可以被人猜」。所以这次第一件事是：<strong>把密码登录这条路彻底关掉</strong>，改成密钥。</p><h2 id="SSH-换密钥"><a href="#SSH-换密钥" class="headerlink" title="SSH 换密钥"></a>SSH 换密钥</h2><p>本地生成一对 ed25519（比 RSA 短、比 RSA 快、强度还更高）：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ssh-keygen -t ed25519 -f ~/.ssh/cyk_server -N <span class="string">&quot;&quot;</span></span><br></pre></td></tr></table></figure><p>把公钥追加到服务器的 <code>~/.ssh/authorized_keys</code>，权限必须是这套：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">chmod</span> 700 ~/.ssh</span><br><span class="line"><span class="built_in">chmod</span> 600 ~/.ssh/authorized_keys</span><br></pre></td></tr></table></figure><p>然后写一份加固配置，放在 <code>sshd_config.d/</code> 下（注意文件名要排在云厂商的默认配置<strong>前面</strong>，因为 sshd 是「先读到的生效」）：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="attribute">PasswordAuthentication</span> <span class="literal">no</span></span><br><span class="line">KbdInteractiveAuthentication <span class="literal">no</span></span><br><span class="line">PermitRootLogin prohibit-password</span><br><span class="line">MaxAuthTries <span class="number">3</span></span><br></pre></td></tr></table></figure><p><code>prohibit-password</code> 的意思不是「允许 root 用密码」，而是「root 只能用密钥」——等于把 root 远程登录变相禁掉了。</p><p>改完先 <code>sshd -t</code> 验证语法，再 reload。<strong>验证前不要关当前窗口</strong>，不然配错了就真的进不去了。</p><p>现在登录长这样：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ssh cyk-server</span><br></pre></td></tr></table></figure><p><code>.ssh/config</code> 里配好别名，不用记 IP、不用带 <code>-i</code>。</p><h2 id="端口从-12-个砍到-3-个"><a href="#端口从-12-个砍到-3-个" class="headerlink" title="端口从 12 个砍到 3 个"></a>端口从 12 个砍到 3 个</h2><p>老机器上开了一堆端口：MySQL、Redis、Nacos、MinIO 控制台……每个都是当年「临时测一下」留下的。</p><p>新机器只留：</p><table><thead><tr><th>端口</th><th>用途</th></tr></thead><tbody><tr><td>22</td><td>SSH</td></tr><tr><td>80</td><td>HTTP（301 跳 HTTPS）</td></tr><tr><td>443</td><td>HTTPS</td></tr></tbody></table><p>图床的 9000 和 9090 全部改成只监听 <code>127.0.0.1</code>，需要的时候用 SSH 隧道临时打开：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ssh -L 9000:127.0.0.1:9000 -L 9090:127.0.0.1:9090 ubuntu@ip -N</span><br></pre></td></tr></table></figure><p>隧道比开放端口好在：<strong>它要求先有 SSH 密钥</strong>。开端口是「对全世界开一扇门」，隧道是「拿着钥匙才给你开」。</p><h2 id="fail2ban"><a href="#fail2ban" class="headerlink" title="fail2ban"></a>fail2ban</h2><p>老机器被打了 4466 次，一次告警都没有。这次装上：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">apt install fail2ban</span><br></pre></td></tr></table></figure><p>默认策略是「10 分钟内失败 5 次封 10 分钟」——10 分钟太短了，换个时间再来就跟没封一样。改成封一天：</p><figure class="highlight ini"><table><tr><td class="code"><pre><span class="line"><span class="comment"># /etc/fail2ban/jail.local</span></span><br><span class="line"><span class="section">[DEFAULT]</span></span><br><span class="line"><span class="attr">bantime</span> = <span class="number">86400</span></span><br><span class="line"><span class="attr">findtime</span> = <span class="number">600</span></span><br><span class="line"><span class="attr">maxretry</span> = <span class="number">5</span></span><br></pre></td></tr></table></figure><p>有个副作用要记住：<strong>自己输错 5 次也会被封一天</strong>。被 DROP 掉的表现是「连接超时」而不是「拒绝连接」，很容易误判成网络问题。解封：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">fail2ban-client <span class="built_in">set</span> sshd unbanip 你的IP</span><br></pre></td></tr></table></figure><h2 id="服务不再直接对外"><a href="#服务不再直接对外" class="headerlink" title="服务不再直接对外"></a>服务不再直接对外</h2><p>图床（MinIO）收到内网，对外只通过 nginx 反代一个路径：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="section">location</span><span class="regexp"> ^~</span> /cykblog/ &#123;</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://127.0.0.1:9000;</span><br><span class="line">    <span class="attribute">add_header</span> Cache-Control <span class="string">&quot;public, max-age=2592000&quot;</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里有个坑：前缀 location 必须写 <code>^~</code>。否则下面那条「静态资源后缀」的正则 location 会优先匹配，把 <code>/cykblog/xxx.webp</code> 当成磁盘文件去找，直接 404。</p><p>文件管理（FileBrowser）也收到内网，但因为它有网页界面、还要长期对外，所以在 nginx 前面又加了一道 Basic Auth：</p><figure class="highlight nginx"><table><tr><td class="code"><pre><span class="line"><span class="section">location</span><span class="regexp"> ^~</span> /files/ &#123;</span><br><span class="line">    <span class="attribute">auth_basic</span> <span class="string">&quot;Restricted&quot;</span>;</span><br><span class="line">    <span class="attribute">auth_basic_user_file</span> /etc/nginx/.htpasswd;</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://127.0.0.1:8081;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>密码文件用 openssl 直接生成，不用额外装包：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">printf</span> <span class="string">&#x27;user:%s\n&#x27;</span> <span class="string">&quot;<span class="subst">$(openssl passwd -apr1 &#x27;你的密码&#x27;)</span>&quot;</span> &gt; /etc/nginx/.htpasswd</span><br></pre></td></tr></table></figure><p>现在访问 <code>/files/</code> 是两层：先进 nginx 的认证，再进 FileBrowser 自己的登录页。这套软件官方已经停止维护了，多一层能挡住「拿已知漏洞直接打」的自动化扫描。</p><h2 id="证书"><a href="#证书" class="headerlink" title="证书"></a>证书</h2><p>Let’s Encrypt，certbot 自动续期，90 天一轮。装完确认一下定时任务在：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">systemctl list-timers | grep certbot</span><br></pre></td></tr></table></figure><h2 id="一个反直觉的点"><a href="#一个反直觉的点" class="headerlink" title="一个反直觉的点"></a>一个反直觉的点</h2><p>加固做完之后，日常使用<strong>几乎没有变麻烦</strong>：</p><ul><li>SSH 还是敲一句，只是不用输密码了</li><li>图床上传还是拖文件，只是先开个隧道（或者用 PicGo 配好一次）</li><li>网页访问完全没区别</li></ul><p>真正变麻烦的只有一件事：<strong>换新设备的时候要加一次公钥</strong>。三十秒的事。</p><p>反过来想，那台老机器省下的这三十秒，代价是两周的 root 权限在别人手里。</p>]]>
    </content>
    <id>https://cyk.pink/posts/3d5f8b17.html</id>
    <link href="https://cyk.pink/posts/3d5f8b17.html"/>
    <published>2026-10-27T01:20:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="重装之后的第一件事不是装软件"><a href="#重装之后的第一件事不是装软件" class="headerlink" title="重装之后的第一件事不是装软件"></a>重装之后的第一件事不是装软件</h2><p>系统重装完，<code>ssh root@ip</code> 一通，密码进去——<strong>这一步就已经错了</strong>。</p>
<p>上一台机器出事，本质就是「密码可以被人猜」。所以这次第一件事是：<strong>把密码登录这条路彻底关掉</strong>，改成密钥。</p>
<h2 id="SSH-换密钥"><a href="#SSH-换密钥" class="headerlink" title="SSH 换密钥"></a>SSH 换密钥</h2><p>本地生成一对 ed25519（比 RSA 短、比 RSA 快、强度还更高）：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ssh-keygen -t]]>
    </summary>
    <title>重装只是开始：把公网服务器从裸奔状态拉回来</title>
    <updated>2026-10-27T01:20:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="运维笔记" scheme="https://cyk.pink/categories/%E8%BF%90%E7%BB%B4%E7%AC%94%E8%AE%B0/"/>
    <category term="安全" scheme="https://cyk.pink/tags/%E5%AE%89%E5%85%A8/"/>
    <category term="服务器" scheme="https://cyk.pink/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8/"/>
    <category term="入侵复盘" scheme="https://cyk.pink/tags/%E5%85%A5%E4%BE%B5%E5%A4%8D%E7%9B%98/"/>
    <category term="运维" scheme="https://cyk.pink/tags/%E8%BF%90%E7%BB%B4/"/>
    <content>
      <![CDATA[<h2 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h2><p>一直挺得意那台轻量服务器——2 核 4G，跑着博客、图床、一个文件管理，两年没出过事。</p><p>10 月 26 号晚上翻图床的时候发现不对劲：桶里多了几个从来没上传过的文件，名字是 <code>shell.php</code>、<code>.htaccess</code>，还有一个叫 <code>ahk_probe</code> 的东西。第一反应是自己手滑，第二反应是——我根本没开过 PHP。</p><h2 id="往下挖"><a href="#往下挖" class="headerlink" title="往下挖"></a>往下挖</h2><p>先看的不是网站，是进程。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ps auxf | grep -v <span class="string">&#x27;\[&#x27;</span> </span><br></pre></td></tr></table></figure><p>排在最上面的几个进程看着很正常，名字是系统自带的那种。但路径不对：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">/dev/shm/.collector/collector.d</span><br></pre></td></tr></table></figure><p><code>/dev/shm</code> 是内存盘，重启就清空，正常服务不会往这儿放东西。<code>collector.d</code> 这个名字起得很「正经」，伪装成日志收集器——这种命名习惯不是随手写的，是有人专门研究过怎么不被人一眼看出来。</p><p>再看 <code>/root/.ssh/</code>，<code>authorized_keys</code> 里多了一把不属于我的公钥。也就是说：<strong>对方不需要密码也能随时回来。</strong></p><h2 id="溯源：入口在哪"><a href="#溯源：入口在哪" class="headerlink" title="溯源：入口在哪"></a>溯源：入口在哪</h2><p>翻 nginx 和 MinIO 的日志，链条很清楚：</p><ol><li>图床的 9000 端口<strong>直接对着公网开着</strong></li><li>MinIO 用的是默认凭据 <code>minioadmin / minioadmin</code>（建的时候图省事，一直没改）</li><li>对方扫到 9000，用默认口令直接进来了</li><li>通过 MinIO 的写入权限往桶里丢 web shell 和探测脚本</li><li>拿到执行权限后提权、装后门、留公钥</li></ol><p>整个过程没有任何「高难度操作」。用的是<strong>出厂默认密码</strong>和<strong>不必要对外开放的端口</strong>。</p><p>顺带在 <code>auth.log</code> 里翻了 SSH 爆破记录，三个来源，都是同一批端口、同一套字典：</p><table><thead><tr><th>来源地址</th><th>归属</th><th>行为</th></tr></thead><tbody><tr><td><code>51.222.47.156</code></td><td>境外（OVH 地址段）</td><td>单 IP 尝试登录 <strong>4466 次</strong></td></tr><tr><td><code>106.53.117.97</code></td><td>中国·广东·广州（腾讯云）</td><td>扫描同一批端口</td></tr><tr><td><code>182.134.27.185</code></td><td>中国·四川·宜宾（电信）</td><td>扫描同一批端口</td></tr></tbody></table><p>没什么好洗的，就是三个贱人：</p><ul><li><code>51.222.47.156</code>（境外 OVH 段）—— 一个 IP 硬试了 4466 次</li><li><code>106.53.117.97</code>（广东广州，腾讯云）</li><li><code>182.134.27.185</code>（四川宜宾，电信）</li></ul><p>租台云主机来扫端口是贱人，拿别人的机器当跳板更是贱人。这三个地址我记在这儿了，哪天想起来了就翻出来看一眼。</p><p>骂完还得防。真正让它们无从下手的是「密码登录关掉」和「端口不对外」。</p><h2 id="止损"><a href="#止损" class="headerlink" title="止损"></a>止损</h2><p>发现后没做「清理」这种操作，直接按事故处理：</p><ol><li><strong>先关机</strong>，切断对方的操作通道——清理是在跟对手赛跑，没必要</li><li>关机后<strong>打快照</strong>，把现场固定下来（这台机器上的一切都可能是证据）</li><li>重启挂个临时环境，只对自己的 IP 开放，把<strong>数据捞出来</strong></li><li>数据核对无误后，<strong>整机重装</strong></li></ol><p>这里有个判断很关键：<strong>不要试图「删掉后门」然后继续用</strong>。一个拿到过 root 的人，可以在任何地方留东西——内核模块、cron、systemd、甚至固件。你不可能穷举。</p><p>唯一可靠的处置是重装。</p><h2 id="数据是怎么保住的"><a href="#数据是怎么保住的" class="headerlink" title="数据是怎么保住的"></a>数据是怎么保住的</h2><p>图床数据在对象存储的桶里，站点是纯静态文件，都落在数据盘上。把这两个目录打包：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">tar czf backup.tar.gz /数据目录/minio/data /var/www</span><br></pre></td></tr></table></figure><p>打完校验 SHA256，传到本地再核一遍。中间出过一次传输损坏，第二次才拿到一致的哈希——<strong>这一步千万不能省</strong>，一个坏包等于没备份。</p><h2 id="教训"><a href="#教训" class="headerlink" title="教训"></a>教训</h2><p>三条，按严重程度排：</p><table><thead><tr><th>问题</th><th>后果</th></tr></thead><tbody><tr><td><strong>默认密码</strong>（minioadmin&#x2F;minioadmin）</td><td>等于没设密码，扫描器字典第一条就是它</td></tr><tr><td><strong>端口对公网敞开</strong>（12 个端口）</td><td>每个端口都是一个入口，图床根本不需要公网直连</td></tr><tr><td><strong>root 密码复用且偏弱</strong></td><td>一旦进来，横向到系统层毫无阻力</td></tr></tbody></table><p>还有一个隐性的：<strong>我从来没看过那台机器的日志</strong>。4466 次爆破，失败了 4466 次，没有一次触发告警，因为压根没装 fail2ban。</p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>这次没人身损失，数据也都回来了。但那种「自己的机器上跑着别人的进程」的感觉，比丢数据更让人不舒服。</p><p>后面几篇会写重装之后做了什么——SSH 换密钥、端口砍到 3 个、fail2ban、图床收回内网、评论系统搬回自己机器。都是很基础的东西，基础到出事之前我一直觉得「没必要」。</p>]]>
    </content>
    <id>https://cyk.pink/posts/7c2a91e4.html</id>
    <link href="https://cyk.pink/posts/7c2a91e4.html"/>
    <published>2026-10-26T14:40:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h2><p>一直挺得意那台轻量服务器——2 核 4G，跑着博客、图床、一个文件管理，两年没出过事。</p>
<p>10 月 26 号晚上翻图床的时候发现不对劲：桶里多了几个从来没上传过的文件，名字是 <code>shell.php</code>、<code>.htaccess</code>，还有一个叫 <code>ahk_probe</code> 的东西。第一反应是自己手滑，第二反应是——我根本没开过 PHP。</p>
<h2 id="往下挖"><a href="#往下挖" class="headerlink" title="往下挖"></a>往下挖</h2><p>先看的不是网站，是进程。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">ps auxf | grep -v <span]]>
    </summary>
    <title>服务器被拿去当肉鸡：一次入侵的完整复盘</title>
    <updated>2026-10-26T14:40:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="工具笔记" scheme="https://cyk.pink/categories/%E5%B7%A5%E5%85%B7%E7%AC%94%E8%AE%B0/"/>
    <category term="工具" scheme="https://cyk.pink/tags/%E5%B7%A5%E5%85%B7/"/>
    <category term="终端" scheme="https://cyk.pink/tags/%E7%BB%88%E7%AB%AF/"/>
    <category term="Shell" scheme="https://cyk.pink/tags/Shell/"/>
    <category term="命令行" scheme="https://cyk.pink/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
    <content>
      <![CDATA[<p>oh-my-posh 是个跨平台的终端提示符美化工具，支持 PowerShell、bash、zsh、fish、cmd。它用 JSON 配置文件定义提示符长什么样，可以显示当前目录、git 分支、执行时间、电池、天气、各种编程语言版本等。</p><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>Windows 上最方便的是用 winget：</p><figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line">winget install JanDeDobbeleer.OhMyPosh</span><br></pre></td></tr></table></figure><p>装完会多出一个 <code>oh-my-posh.exe</code>。macOS 和 Linux 可以用 Homebrew：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">brew install jandedobbeleer/oh-my-posh/oh-my-posh</span><br></pre></td></tr></table></figure><p>或者直接用官方安装脚本：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">curl -s https://ohmyposh.dev/install.sh | bash</span><br></pre></td></tr></table></figure><h2 id="字体"><a href="#字体" class="headerlink" title="字体"></a>字体</h2><p>oh-my-posh 的很多主题用到 Nerd Font 图标。如果看到方块或乱码，说明当前字体没有这些字形。</p><p>推荐装 MesloLGM Nerd Font 或 Cascadia Code PL。Windows Terminal 里在设置 → 外观 → 字体里选上。VS Code 终端同样要在 <code>settings.json</code> 里配 <code>terminal.integrated.fontFamily</code>。</p><h2 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h2><p>根据你的 shell 加一句启动命令。</p><p>PowerShell（加到 <code>$PROFILE</code>）：</p><figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line"><span class="built_in">oh</span><span class="literal">-my-posh</span> init pwsh | <span class="built_in">Invoke-Expression</span></span><br></pre></td></tr></table></figure><p>bash（加到 <code>~/.bashrc</code>）：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">eval</span> <span class="string">&quot;<span class="subst">$(oh-my-posh init bash)</span>&quot;</span></span><br></pre></td></tr></table></figure><p>zsh（加到 <code>~/.zshrc</code>）：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">eval</span> <span class="string">&quot;<span class="subst">$(oh-my-posh init zsh)</span>&quot;</span></span><br></pre></td></tr></table></figure><p>fish（加到 <code>~/.config/fish/config.fish</code>）：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">oh-my-posh init fish | source</span><br></pre></td></tr></table></figure><h2 id="换主题"><a href="#换主题" class="headerlink" title="换主题"></a>换主题</h2><p>初始化时可以直接指定主题：</p><figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line"><span class="built_in">oh</span><span class="literal">-my-posh</span> init pwsh <span class="literal">--config</span> <span class="string">&quot;<span class="variable">$env:POSH_THEMES_PATH</span>\jandedobbeleer.omp.json&quot;</span> | <span class="built_in">Invoke-Expression</span></span><br></pre></td></tr></table></figure><p>官方内置了很多主题，装完后主题文件一般在：</p><ul><li>Windows：<code>%LOCALAPPDATA%\Programs\oh-my-posh\themes\</code></li><li>macOS&#x2F;Linux：<code>~/.cache/oh-my-posh/themes/</code></li></ul><p>想试效果可以临时运行：</p><figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line"><span class="built_in">oh</span><span class="literal">-my-posh</span> print primary <span class="literal">--config</span> <span class="string">&quot;C:\path\to\theme.omp.json&quot;</span></span><br></pre></td></tr></table></figure><h2 id="自己改配置"><a href="#自己改配置" class="headerlink" title="自己改配置"></a>自己改配置</h2><p>主题就是 JSON。一个最小结构长这样：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;$schema&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="number">2</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;blocks&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;prompt&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;alignment&quot;</span><span class="punctuation">:</span> <span class="string">&quot;left&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;segments&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">          <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;path&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;style&quot;</span><span class="punctuation">:</span> <span class="string">&quot;plain&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;foreground&quot;</span><span class="punctuation">:</span> <span class="string">&quot;#26C6DA&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;template&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&#123;&#123; .Path &#125;&#125; &quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">          <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;git&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;style&quot;</span><span class="punctuation">:</span> <span class="string">&quot;plain&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;foreground&quot;</span><span class="punctuation">:</span> <span class="string">&quot;#FFEB3B&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;template&quot;</span><span class="punctuation">:</span> <span class="string">&quot;(&#123;&#123; .HEAD &#125;&#125;)&quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span></span><br><span class="line">      <span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>segment 类型很多：path、git、executiontime、battery、node、python、go、kubectl……官方文档有完整列表。</p><h2 id="多台机器同步"><a href="#多台机器同步" class="headerlink" title="多台机器同步"></a>多台机器同步</h2><p>把主题文件放到 git 仓库或网盘里同步，初始化时指向固定路径。或者用环境变量 <code>POSH_THEME</code> 指向配置，这样换机器只需改一个变量。</p><figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line"><span class="variable">$env:POSH_THEME</span> = <span class="string">&quot;C:\Users\codem\.config\oh-my-posh\mytheme.omp.json&quot;</span></span><br><span class="line"><span class="built_in">oh</span><span class="literal">-my-posh</span> init pwsh <span class="literal">--config</span> <span class="variable">$env:POSH_THEME</span> | <span class="built_in">Invoke-Expression</span></span><br></pre></td></tr></table></figure><h2 id="值不值得装"><a href="#值不值得装" class="headerlink" title="值不值得装"></a>值不值得装</h2><p>如果你每天大量时间在终端里，一个信息丰富、一眼能看清上下文状态的提示符确实能省不少事。oh-my-posh 配置一次，跨 shell 复用，维护成本不高。</p>]]>
    </content>
    <id>https://cyk.pink/posts/85891fe1.html</id>
    <link href="https://cyk.pink/posts/85891fe1.html"/>
    <published>2026-08-05T02:00:00.000Z</published>
    <summary>
      <![CDATA[<p>oh-my-posh 是个跨平台的终端提示符美化工具，支持 PowerShell、bash、zsh、fish、cmd。它用 JSON 配置文件定义提示符长什么样，可以显示当前目录、git 分支、执行时间、电池、天气、各种编程语言版本等。</p>
<h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>Windows 上最方便的是用 winget：</p>
<figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line">winget install JanDeDobbeleer.OhMyPosh</span><br></pre></td></tr></table></figure>

<p>装完会多出一个 <code>oh-my-posh.exe</code>。macOS 和 Linux 可以用 Homebrew：</p>
<figure class="highlight]]>
    </summary>
    <title>oh-my-posh：一个好看的终端提示符怎么装</title>
    <updated>2026-08-05T02:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="Python" scheme="https://cyk.pink/tags/Python/"/>
    <category term="DOCX" scheme="https://cyk.pink/tags/DOCX/"/>
    <category term="LaTeX" scheme="https://cyk.pink/tags/LaTeX/"/>
    <category term="Markdown" scheme="https://cyk.pink/tags/Markdown/"/>
    <category term="工具" scheme="https://cyk.pink/tags/%E5%B7%A5%E5%85%B7/"/>
    <content>
      <![CDATA[<p>DOCX Pipeline 是个把 Markdown 转成中文 DOCX 的命令行工具。双后端：Pure Python 零外部依赖，Pandoc 质量更高。v1.2.0 补上了纯 Python 后端对 LaTeX 数学公式的支持，写论文、出报告时不用硬装 Pandoc。</p><p>下面先说怎么用，再简单提实现。</p><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>Python 3.10 以上：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pip install git+https://github.com/redamancy231-create/docx-pipeline.git</span><br></pre></td></tr></table></figure><p>核心依赖会一起装好：<code>python-docx</code>、<code>PyYAML</code>、<code>click</code>、<code>Pillow</code>。Pandoc 和 mermaid-cli 是可选的，后面按需安装。</p><h2 id="一分钟上手"><a href="#一分钟上手" class="headerlink" title="一分钟上手"></a>一分钟上手</h2><h3 id="1-初始化项目"><a href="#1-初始化项目" class="headerlink" title="1. 初始化项目"></a>1. 初始化项目</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docx-pipeline init --project-dir ./my-doc --template report --name <span class="string">&quot;技术报告&quot;</span></span><br></pre></td></tr></table></figure><p>执行完会在 <code>./my-doc</code> 下生成 <code>project.yaml</code>，里面包含页面、字体、路径、后端开关等所有配置。</p><p>四个内置模板：</p><table><thead><tr><th>模板</th><th>场景</th><th>默认后端</th></tr></thead><tbody><tr><td><code>default</code></td><td>通用中文文档</td><td>Pure Python</td></tr><tr><td><code>academic</code></td><td>论文&#x2F;学位论文</td><td>Pure Python</td></tr><tr><td><code>report</code></td><td>技术报告，带目录和 Mermaid</td><td>Pandoc</td></tr><tr><td><code>strategy</code></td><td>量化策略文档</td><td>Pure Python</td></tr></tbody></table><h3 id="2-写-Markdown"><a href="#2-写-Markdown" class="headerlink" title="2. 写 Markdown"></a>2. 写 Markdown</h3><p>模板会给你一个入口文件路径，默认在 <code>./md/main.md</code>。直接写普通 Markdown 就行，公式用 <code>$...$</code> 行内、<code>$$...$$</code> 独立：</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line">---</span><br><span class="line"><span class="section">title: 示例报告</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line"><span class="section"># 正文</span></span><br><span class="line"></span><br><span class="line">考虑正弦信号的采样定理：</span><br><span class="line"></span><br><span class="line">$$f<span class="emphasis">_s \geq 2 f_</span>&#123;\max&#125;$$</span><br><span class="line"></span><br><span class="line">若采样率 $f<span class="emphasis">_s = 100\,\text&#123;Hz&#125;$，则奈奎斯特频率为 $f_</span>N = f<span class="emphasis">_s/2 = 50\,\text&#123;Hz&#125;$。</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">## 表格</span></span><br><span class="line"><span class="emphasis"></span></span><br><span class="line"><span class="emphasis">| 参数 | 值 |</span></span><br><span class="line"><span class="emphasis">|------|-----|</span></span><br><span class="line"><span class="emphasis">| 采样间隔 | 0.01 s |</span></span><br><span class="line"><span class="emphasis">| 窗长 | 21 |</span></span><br></pre></td></tr></table></figure><h3 id="3-转换"><a href="#3-转换" class="headerlink" title="3. 转换"></a>3. 转换</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cd</span> ./my-doc</span><br><span class="line">docx-pipeline convert --config ./project.yaml</span><br></pre></td></tr></table></figure><p>输出路径由 <code>project.yaml</code> 里的 <code>paths.docx_output</code> 决定，默认 <code>./output/doc.docx</code>。</p><p>想换输出文件：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docx-pipeline convert --config ./project.yaml --output ./out/报告.docx</span><br></pre></td></tr></table></figure><p>只想看会不会报错，不生成文件：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docx-pipeline convert --config ./project.yaml --dry-run</span><br></pre></td></tr></table></figure><h2 id="常用命令"><a href="#常用命令" class="headerlink" title="常用命令"></a>常用命令</h2><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 查看配置摘要</span></span><br><span class="line">docx-pipeline info --config ./project.yaml</span><br><span class="line"></span><br><span class="line"><span class="comment"># 校验配置合法性</span></span><br><span class="line">docx-pipeline validate --config ./project.yaml</span><br><span class="line"></span><br><span class="line"><span class="comment"># 强制用 Pure Python 后端</span></span><br><span class="line">docx-pipeline convert --config ./project.yaml --method pure</span><br><span class="line"></span><br><span class="line"><span class="comment"># 强制用 Pandoc 后端</span></span><br><span class="line">docx-pipeline convert --config ./project.yaml --method pandoc</span><br><span class="line"></span><br><span class="line"><span class="comment"># 传额外参数给 pandoc</span></span><br><span class="line">docx-pipeline convert --config ./project.yaml --pandoc-args <span class="string">&quot;--number-sections&quot;</span></span><br></pre></td></tr></table></figure><p><code>init</code> 和 <code>convert</code> 是日常使用最多的两个命令。</p><h2 id="配置重点"><a href="#配置重点" class="headerlink" title="配置重点"></a>配置重点</h2><p><code>project.yaml</code> 是全部行为的入口。几个常见调整：</p><p><strong>改输入输出路径</strong></p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">paths:</span></span><br><span class="line">  <span class="attr">md_source:</span> <span class="string">&quot;./chapters/main.md&quot;</span></span><br><span class="line">  <span class="attr">docx_output:</span> <span class="string">&quot;./output/报告.docx&quot;</span></span><br></pre></td></tr></table></figure><p><strong>改字体</strong></p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">fonts:</span></span><br><span class="line">  <span class="attr">east_asian:</span> <span class="string">&quot;微软雅黑&quot;</span></span><br><span class="line">  <span class="attr">latin:</span> <span class="string">&quot;Times New Roman&quot;</span></span><br></pre></td></tr></table></figure><p>字体必须在系统里已安装，否则 Word 会回退默认字体。</p><p><strong>页面和缩进</strong></p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">page:</span></span><br><span class="line">  <span class="attr">size:</span> <span class="string">&quot;A4&quot;</span></span><br><span class="line">  <span class="attr">orientation:</span> <span class="string">&quot;portrait&quot;</span></span><br><span class="line">  <span class="attr">margins:</span></span><br><span class="line">    <span class="attr">top:</span> <span class="number">2.54</span></span><br><span class="line">    <span class="attr">bottom:</span> <span class="number">2.54</span></span><br><span class="line">    <span class="attr">left:</span> <span class="number">3.18</span></span><br><span class="line">    <span class="attr">right:</span> <span class="number">3.18</span></span><br><span class="line"></span><br><span class="line"><span class="attr">styles:</span></span><br><span class="line">  <span class="attr">paragraph:</span></span><br><span class="line">    <span class="attr">line_spacing:</span> <span class="number">1.15</span></span><br><span class="line">    <span class="attr">first_line_indent:</span> <span class="number">0.0</span>   <span class="comment"># cm，学术论文可设 0.74 左右</span></span><br></pre></td></tr></table></figure><p><strong>后端开关</strong></p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">pandoc:</span></span><br><span class="line">  <span class="attr">enabled:</span> <span class="literal">false</span>          <span class="comment"># true 启用 Pandoc</span></span><br><span class="line">  <span class="attr">extra_args:</span> []</span><br><span class="line">  <span class="attr">reference_docx:</span> <span class="string">&quot;&quot;</span>      <span class="comment"># Pandoc 参考文档路径</span></span><br></pre></td></tr></table></figure><p><code>enabled: false</code> 时走 Pure Python，LaTeX 公式由 v1.2.0 新增的 <code>latex2mathml</code> → MathML → OMML 桥转换。<code>enabled: true</code> 时走 Pandoc，公式渲染质量更好，但需要系统里有 pandoc。</p><h2 id="后端怎么选"><a href="#后端怎么选" class="headerlink" title="后端怎么选"></a>后端怎么选</h2><ul><li><strong>不想装 pandoc</strong> → Pure Python，一个 Python 环境就够了，公式也能转。</li><li><strong>要代码语法高亮、复杂表格、章节自动编号</strong> → Pandoc。</li><li><strong>想精确控制中文字体、缩进、行距</strong> → Pure Python 更直接。</li></ul><p>Auto 模式（默认）会先看 <code>pandoc.enabled</code>，再看 pandoc 是否在 PATH 里，能 pandoc 就 pandoc，不能就自动降级到 Pure Python。</p><h2 id="Mermaid-图表"><a href="#Mermaid-图表" class="headerlink" title="Mermaid 图表"></a>Mermaid 图表</h2><p><code>report</code> 模板默认开启 Mermaid。需要 Node.js 和 mermaid-cli：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install -g @mermaid-js/mermaid-cli</span><br></pre></td></tr></table></figure><p>Markdown 里写：</p><figure class="highlight markdown"><table><tr><td class="code"><pre><span class="line"><span class="code">```mermaid</span></span><br><span class="line"><span class="code">flowchart LR</span></span><br><span class="line"><span class="code">    A[输入] --&gt; B[处理] --&gt; C[输出]</span></span><br><span class="line"><span class="code">```</span></span><br></pre></td></tr></table></figure><p>转换时会自动渲染成 PNG 嵌入 DOCX。渲染失败的代码块会保留原文并加警告注释。</p><h2 id="v1-2-0-公式实现简说"><a href="#v1-2-0-公式实现简说" class="headerlink" title="v1.2.0 公式实现简说"></a>v1.2.0 公式实现简说</h2><p>纯 Python 后端新增了一条 LaTeX → Word 原生公式的路径：</p><ol><li>扫描 Markdown，把 <code>$...$</code> 和 <code>$$...$$</code> 抠出来，替换成占位符。</li><li>用 <code>latex2mathml</code> 把 LaTeX 转成 MathML。</li><li>用 <code>lxml</code> 遍历 MathML 树，生成 Word 的 <code>m:oMath</code> &#x2F; <code>m:oMathPara</code>。</li><li>行内公式挂到段落里，独立公式包成居中段落。</li><li>转换失败就回退成原始 LaTeX 文本，不会让整个文档崩掉。</li></ol><p>覆盖分数、根号、上下标、极限、n 元运算符、重音、矩阵、括号、围栏等 14 种结构，有 21 个自动化用例校验。</p><p>不过说实话，Pure Python 的 OMML 渲染在分式、根号、大型运算符等复杂式子上还是有缺陷；Pandoc 后端十五年打磨的 TeX 引擎更稳。所以能装 pandoc 的话，复杂公式建议优先 Pandoc 后端。</p><h2 id="两个容易踩的坑"><a href="#两个容易踩的坑" class="headerlink" title="两个容易踩的坑"></a>两个容易踩的坑</h2><p><strong>Windows 乱码</strong></p><p>中文输出在 Windows 下可能乱码，加环境变量：</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">export</span> PYTHONIOENCODING=utf-8</span><br></pre></td></tr></table></figure><p>PowerShell：</p><figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line"><span class="variable">$env:PYTHONIOENCODING</span> = <span class="string">&quot;utf-8&quot;</span></span><br></pre></td></tr></table></figure><p><strong>目录重复</strong></p><p>如果 Markdown 里已经手写了一个”目录”章节，pipeline 又会自动插入 Word 的 TOC 域，结果出现双目录。要么在 Markdown 里删掉手工目录，要么在 <code>project.yaml</code> 里设 <code>styles.toc.enabled: false</code>。</p><h2 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h2><p>DOCX Pipeline 让 Markdown → Word 这件事可以一条命令跑完；v1.2.0 之后，公式也不需要 Pandoc 了。写报告、论文、技术文档时，它能省下大量调格式的时间。</p>]]>
    </content>
    <id>https://cyk.pink/posts/23eb6558.html</id>
    <link href="https://cyk.pink/posts/23eb6558.html"/>
    <published>2026-07-22T08:00:00.000Z</published>
    <summary>
      <![CDATA[<p>DOCX Pipeline 是个把 Markdown 转成中文 DOCX 的命令行工具。双后端：Pure Python 零外部依赖，Pandoc 质量更高。v1.2.0 补上了纯 Python 后端对 LaTeX 数学公式的支持，写论文、出报告时不用硬装 Pandoc。</p>
<p>下面先说怎么用，再简单提实现。</p>
<h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>Python 3.10 以上：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pip install]]>
    </summary>
    <title>DOCX Pipeline v1.2.0：Markdown 转 Word，顺便把 LaTeX 公式也塞进去</title>
    <updated>2026-07-22T08:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="GPR" scheme="https://cyk.pink/tags/GPR/"/>
    <category term="雷达" scheme="https://cyk.pink/tags/%E9%9B%B7%E8%BE%BE/"/>
    <category term="信号处理" scheme="https://cyk.pink/tags/%E4%BF%A1%E5%8F%B7%E5%A4%84%E7%90%86/"/>
    <category term="平滑" scheme="https://cyk.pink/tags/%E5%B9%B3%E6%BB%91/"/>
    <content>
      <![CDATA[<p>增益把深部放大后，一些横向窄的条纹、绕射尾、振铃尾也跟着被放大，在 B-scan 上冒成一片斜纹或竖条毛刺。横向平滑就是沿测线方向做均值替换，把这些窄东西糊掉，横向连续的层位则保留。</p><h2 id="和背景去除的区别"><a href="#和背景去除的区别" class="headerlink" title="和背景去除的区别"></a>和背景去除的区别</h2><p>背景去除是「减均值」：删掉横向共有的成分，留下横向局部的点目标。</p><p>横向平滑是「替换为均值」：把每道替换成左右几道的平均，抹掉横向局部的窄条纹，留下横向共有的连续层位。</p><p>两者保留的东西正好相反：</p><table><thead><tr><th></th><th>操作</th><th>去掉</th><th>保留</th></tr></thead><tbody><tr><td>背景去除</td><td>减均值</td><td>横向共有（直达波、水平横条）</td><td>横向局部（钢筋双曲线）</td></tr><tr><td>横向平滑</td><td>替换为均值</td><td>横向局部（绕射尾、条纹）</td><td>横向共有（连续层位）</td></tr></tbody></table><h2 id="单级盒式的问题"><a href="#单级盒式的问题" class="headerlink" title="单级盒式的问题"></a>单级盒式的问题</h2><p>最简单的做法是用一个矩形窗做一级滑动平均。但它的频率响应是 sinc 函数，旁瓣大——阻带里的纹波压不干净，条纹会留残影。</p><h2 id="两级盒式更干净"><a href="#两级盒式更干净" class="headerlink" title="两级盒式更干净"></a>两级盒式更干净</h2><p>做两级串联：第一级用较大窗做主要平滑，第二级用小窗磨掉第一级留下的边缘台阶。两个矩形核卷积后等效成一个梯形核，频响近似 sinc²，旁瓣比单级小得多。</p><p>实测对比（window&#x3D;10）：</p><table><thead><tr><th>归一化频率</th><th>单级响应</th><th>两级响应</th><th>两级压到</th></tr></thead><tbody><tr><td>0.05（通带）</td><td>0.577</td><td>0.558</td><td>97%</td></tr><tr><td>0.15</td><td>0.177</td><td>0.128</td><td>72%</td></tr><tr><td>0.25</td><td>0.091</td><td>0.030</td><td>33%</td></tr><tr><td>0.30</td><td>0.091</td><td>0.012</td><td>13%</td></tr></tbody></table><p>通带内两者差不多，不伤层位；阻带内两级把残余压到单级的 1&#x2F;3~1&#x2F;8。</p><h2 id="梯形核怎么来的"><a href="#梯形核怎么来的" class="headerlink" title="梯形核怎么来的"></a>梯形核怎么来的</h2><p>以 window&#x3D;10 为例：第一级半窗 5（11 点核），第二级半窗 1（3 点核）。算中心点输出时，每个位置被几个第二级抽头「看到」：</p><ul><li>中心位置：3 个抽头都看到，权重最大；</li><li>边缘位置：只有 1 个抽头看到，权重最小。</li></ul><p>把「被看到的次数」画出来就是梯形：1, 2, 3, 3, …, 3, 2, 1。</p><h2 id="窗长怎么选"><a href="#窗长怎么选" class="headerlink" title="窗长怎么选"></a>窗长怎么选</h2><p>只有一个参数 <code>window</code>：平滑跨度，单位是道数。</p><ul><li>斜条纹还在：window 调大；</li><li>横向层位被抹糊了：window 调小，或关掉本步；</li><li>window 的物理含义 ≈ 想抹掉的横向窄目标的宽度，一般 8~20。</li></ul><h2 id="副作用"><a href="#副作用" class="headerlink" title="副作用"></a>副作用</h2><ol><li>会抹掉横向窄的真实目标：小孤腔、细裂隙、单根细钢筋都会被糊掉。</li><li>破坏幅值定量：每个值被换成邻域均值，原始振幅信息没了。</li><li>必须放在流水线最后：它破坏幅值，所有需要真实幅值的步骤都要在它之前。</li></ol><h2 id="边界处理"><a href="#边界处理" class="headerlink" title="边界处理"></a>边界处理</h2><p>图的两端窗会超出范围。我的做法是截断边界，只用地道内真实数据，除以实际参与平均的道数。不补零、不镜像，避免引入假象。</p>]]>
    </content>
    <id>https://cyk.pink/posts/bd00ef30.html</id>
    <link href="https://cyk.pink/posts/bd00ef30.html"/>
    <published>2026-07-22T02:00:00.000Z</published>
    <summary>
      <![CDATA[<p>增益把深部放大后，一些横向窄的条纹、绕射尾、振铃尾也跟着被放大，在 B-scan 上冒成一片斜纹或竖条毛刺。横向平滑就是沿测线方向做均值替换，把这些窄东西糊掉，横向连续的层位则保留。</p>
<h2 id="和背景去除的区别"><a href="#和背景去除的区别" class="headerlink"]]>
    </summary>
    <title>横向平滑：抹掉 GPR 剖面上的窄条纹</title>
    <updated>2026-07-22T02:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="踩坑" scheme="https://cyk.pink/tags/%E8%B8%A9%E5%9D%91/"/>
    <category term="图像处理" scheme="https://cyk.pink/tags/%E5%9B%BE%E5%83%8F%E5%A4%84%E7%90%86/"/>
    <category term="数据可视化" scheme="https://cyk.pink/tags/%E6%95%B0%E6%8D%AE%E5%8F%AF%E8%A7%86%E5%8C%96/"/>
    <content>
      <![CDATA[<p>处理一批灰度图，深部信号弱、看不清，就加了道增益（gain）把深部的幅值往上提。调完增益打开图一看，奇怪了——本来是想让深部亮起来，结果观感是浅部反而变灰了，深部也没见亮多少。第一反应是增益写错了，查了半天代码没问题。</p><p>后来才反应过来，这不是增益的 bug，是显示的色阶在捣乱。</p><h2 id="全局百分位定色阶，会被放大后的极值撑大"><a href="#全局百分位定色阶，会被放大后的极值撑大" class="headerlink" title="全局百分位定色阶，会被放大后的极值撑大"></a>全局百分位定色阶，会被放大后的极值撑大</h2><p>显示这种动态范围很大的灰度数据，一般不会直接用最小最大值定 vmin&#x2F;vmax——少数极大值（比如直达波那种）会把范围撑得太大，绝大多数数据挤在一头，图就「非黑即白」。常见的做法是用百分位，比如取第 1 和第 99 百分位作为显示的上下限，把长尾削掉，层次就出来了。</p><p>问题在于，这个百分位是<strong>整张图</strong>算的。增益一上去，深部那些本来不强、但被放大了的反射，幅值被抬得很高，整张图的 p99 跟着水涨船高（实测能从不到一万飙到二十多万）。色阶的上限一拉高，那些<strong>没被增益放大的浅部</strong>数据，在新的色阶里就全被挤向中灰了。</p><p>所以调大增益参数，看到的不是「深部变亮」，而是「浅部变灰」——因为色阶被深部的极值撑宽了，浅部在更宽的尺度里自然显得更暗。本质是灰度级被重新分配了，不是数据本身变暗。</p><h2 id="想看增益的真实效果，得把色阶锁住"><a href="#想看增益的真实效果，得把色阶锁住" class="headerlink" title="想看增益的真实效果，得把色阶锁住"></a>想看增益的真实效果，得把色阶锁住</h2><p>要判断增益到底有没有把深部提亮，得让前后两幅图用<strong>同一把尺子</strong>量。具体就是把「加增益前」那张图的百分位 [1,99] 记下来，加增益后显示的时候还用这同一组 vmin&#x2F;vmax，不让它重新算：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">vmin, vmax = np.percentile(raw, [<span class="number">1</span>, <span class="number">99</span>])   <span class="comment"># 用原始图定一次色阶</span></span><br><span class="line"></span><br><span class="line">ax[<span class="number">0</span>].imshow(raw,    vmin=vmin, vmax=vmax, cmap=<span class="string">&#x27;gray&#x27;</span>)   <span class="comment"># 原图</span></span><br><span class="line">ax[<span class="number">1</span>].imshow(gained, vmin=vmin, vmax=vmax, cmap=<span class="string">&#x27;gray&#x27;</span>)   <span class="comment"># 增益后，共用同一色阶</span></span><br></pre></td></tr></table></figure><p>这样色阶锁死了，增益放大的深部目标是真的「冒出来」变亮，浅部因为没动还是原来的灰度，对比一目了然。配套加个「锁定色阶」的开关，勾上两图共用原始百分位，不勾就各自算各自的——平时看绝对层次用各自的，调增益判效果一定得锁。</p><p>这事给我的一个教训：调一个看起来「没生效」的处理参数，先别急着怀疑算法，想想显示环节是不是也跟着动了。增益改的是数据，百分位色阶也跟着数据重新算，两者一叠加，呈现出来的变化就不是单纯增益的效果了。判断一个处理到底有没有用，得把它放在固定的显示基准下看，不然你调的是参数，被糊弄的其实是色阶。</p>]]>
    </content>
    <id>https://cyk.pink/posts/e4b2a96c.html</id>
    <link href="https://cyk.pink/posts/e4b2a96c.html"/>
    <published>2026-07-15T02:30:00.000Z</published>
    <summary>
      <![CDATA[<p>处理一批灰度图，深部信号弱、看不清，就加了道增益（gain）把深部的幅值往上提。调完增益打开图一看，奇怪了——本来是想让深部亮起来，结果观感是浅部反而变灰了，深部也没见亮多少。第一反应是增益写错了，查了半天代码没问题。</p>
<p>后来才反应过来，这不是增益的 bug，是显示的色阶在捣乱。</p>
<h2 id="全局百分位定色阶，会被放大后的极值撑大"><a href="#全局百分位定色阶，会被放大后的极值撑大" class="headerlink" title="全局百分位定色阶，会被放大后的极值撑大"></a>全局百分位定色阶，会被放大后的极值撑大</h2><p>显示这种动态范围很大的灰度数据，一般不会直接用最小最大值定 vmin&#x2F;vmax——少数极大值（比如直达波那种）会把范围撑得太大，绝大多数数据挤在一头，图就「非黑即白」。常见的做法是用百分位，比如取第 1 和第 99]]>
    </summary>
    <title>给图像做完增益别再用整张图的百分位定色阶</title>
    <updated>2026-07-15T02:30:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="GPR" scheme="https://cyk.pink/tags/GPR/"/>
    <category term="雷达" scheme="https://cyk.pink/tags/%E9%9B%B7%E8%BE%BE/"/>
    <category term="信号处理" scheme="https://cyk.pink/tags/%E4%BF%A1%E5%8F%B7%E5%A4%84%E7%90%86/"/>
    <category term="增益" scheme="https://cyk.pink/tags/%E5%A2%9E%E7%9B%8A/"/>
    <content>
      <![CDATA[<p>手电筒照得越远越暗，雷达波也一样。深部反射回波比浅部弱很多，不补一下，剖面深处就是一片黑。</p><h2 id="深部为什么衰减"><a href="#深部为什么衰减" class="headerlink" title="深部为什么衰减"></a>深部为什么衰减</h2><p>电磁波往下传，能量衰减来自两方面：</p><ul><li>球面扩散：能量摊开，随距离平方变弱（∝ 1&#x2F;r²）；</li><li>介电吸收：介质吸能，随距离指数衰减（∝ e^(−αz)）。</li></ul><p>深部反射体回波可能比浅部弱 40~60dB。</p><h2 id="时间增益：按时间乘一条固定曲线"><a href="#时间增益：按时间乘一条固定曲线" class="headerlink" title="时间增益：按时间乘一条固定曲线"></a>时间增益：按时间乘一条固定曲线</h2><p>造一条随时间单调增长的曲线 $G(t)$，每道信号逐样点乘上去。浅处乘得小，深处乘得大。</p><p>三种常见形状：</p><p>$$G(t)&#x3D;\begin{cases}1+p,t &amp; \text{linear}\ e^{,p,t} &amp; \text{exp}\ t^{,p}\text{ 归一化} &amp; \text{power}\end{cases}$$</p><p>指数型最贴合指数吸收的物理。设 p&#x3D;0.051，dt&#x3D;0.054ns，512 个样点：</p><table><thead><tr><th>位置</th><th>时间</th><th>增益</th><th>相当于</th></tr></thead><tbody><tr><td>浅</td><td>0 ns</td><td>1.00</td><td>+0 dB</td></tr><tr><td>中</td><td>10 ns</td><td>1.66</td><td>+4.4 dB</td></tr><tr><td>深</td><td>27.6 ns</td><td>4.09</td><td>+12.2 dB</td></tr></tbody></table><p>最深处放大约 4 倍，原本看不见的弱反射被拉到可看的亮度。</p><p>不过别搞错：时间增益不是真正的反衰减，真实吸收系数未知。它只是显示补偿，不是物理还原。</p><h2 id="AGC：自动音量"><a href="#AGC：自动音量" class="headerlink" title="AGC：自动音量"></a>AGC：自动音量</h2><p>AGC 像耳机自动音量：强处压小、弱处放大，让每个深度看起来差不多强。</p><p>对每个样点，取前后小窗口算局部 RMS 能量，然后该样点除以这个 RMS：</p><p>$$\text{rms}[n]&#x3D;\sqrt{\frac{1}{W}\sum_{k&#x3D;-W&#x2F;2}^{W&#x2F;2}x[n+k]^2+\varepsilon},\qquad \text{out}[n]&#x3D;\frac{x[n]}{\text{rms}[n]}$$</p><p>强段除以大数变小，弱段除以小数变大，每小段能量都被归一到约 1。</p><h2 id="两者差别"><a href="#两者差别" class="headerlink" title="两者差别"></a>两者差别</h2><table><thead><tr><th></th><th>时间增益</th><th>AGC</th></tr></thead><tbody><tr><td>曲线</td><td>固定预设</td><td>看数据自适应算</td></tr><tr><td>依据</td><td>时间位置</td><td>邻域实际能量</td></tr><tr><td>效果</td><td>温和补深部</td><td>强行抹平强弱差</td></tr><tr><td>副作用</td><td>改变幅值</td><td>彻底破坏幅值</td></tr></tbody></table><h2 id="gain-后会重新引入直流"><a href="#gain-后会重新引入直流" class="headerlink" title="gain 后会重新引入直流"></a>gain 后会重新引入直流</h2><p>即使 gain 前每道已经是零均值，gain 之后均值又可能不等于 0。增益曲线恒正递增，深部样点被放大更多，会把整道均值往深部拽偏。</p><p>举个例子，dewow 后零均值的道：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">x        = [ 1, -1,  1, -1]     均值 = 0</span><br><span class="line">G        = [ 1,  2,  3,  4]     (浅→深递增)</span><br><span class="line">x·G      = [ 1, -2,  3, -4]     均值 = -0.5</span><br><span class="line">减均值后  = [1.5,-1.5,3.5,-4.5]  均值 = 0</span><br></pre></td></tr></table></figure><p>所以有些流程在 gain 后再减一次每道均值，称为 <code>post_gain_dc</code>。它跟 dewow 不是一回事：dewow 是去低频漂移，<code>post_gain_dc</code> 是清 gain 新引入的直流。</p><h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><ul><li>增益&#x2F;AGC 会放大噪声，调太大深部就全是雪花。</li><li>改变幅值关系，不能定量。「反射越强 &#x3D; 病害越重」必须回原始数据。</li><li>AGC 比固定增益更激进，仅用于显示和目标识别，绝不能用于定量。</li></ul><p>AGC 窗长一般取目标回波持续长度的 2~3 倍；固定增益的 p 调到深部看得见、但噪声还没明显冒出来就行。</p>]]>
    </content>
    <id>https://cyk.pink/posts/5c056d5b.html</id>
    <link href="https://cyk.pink/posts/5c056d5b.html"/>
    <published>2026-07-15T02:00:00.000Z</published>
    <summary>
      <![CDATA[<p>手电筒照得越远越暗，雷达波也一样。深部反射回波比浅部弱很多，不补一下，剖面深处就是一片黑。</p>
<h2 id="深部为什么衰减"><a href="#深部为什么衰减" class="headerlink" title="深部为什么衰减"></a>深部为什么衰减</h2><p>电磁波往下传，能量衰减来自两方面：</p>
<ul>
<li>球面扩散：能量摊开，随距离平方变弱（∝ 1&#x2F;r²）；</li>
<li>介电吸收：介质吸能，随距离指数衰减（∝ e^(−αz)）。</li>
</ul>
<p>深部反射体回波可能比浅部弱 40~60dB。</p>
<h2 id="时间增益：按时间乘一条固定曲线"><a href="#时间增益：按时间乘一条固定曲线" class="headerlink" title="时间增益：按时间乘一条固定曲线"></a>时间增益：按时间乘一条固定曲线</h2><p>造一条随时间单调增长的曲线]]>
    </summary>
    <title>GPR 时间增益与 AGC：把深部提亮的两条路</title>
    <updated>2026-07-15T02:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="二进制" scheme="https://cyk.pink/tags/%E4%BA%8C%E8%BF%9B%E5%88%B6/"/>
    <category term="数据处理" scheme="https://cyk.pink/tags/%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86/"/>
    <category term="踩坑" scheme="https://cyk.pink/tags/%E8%B8%A9%E5%9D%91/"/>
    <content>
      <![CDATA[<p>把一批 16 位采样的数据读进来做了点预处理（去直流、去背景这种减法操作），再写回原来的二进制文件给别的专业软件打开。读进来处理的时候一切正常，写回去打开一看，有的文件好好的，有的直接满屏噪点、糊成一团。同样的代码、同样的处理，结果天差地别，卡了好一阵。</p><p>根子是 16 位整数到底按有符号还是无符号解读，不是统一的，取决于文件格式。</p><h2 id="同样-16-位，有的围绕-0-有的围绕-32768"><a href="#同样-16-位，有的围绕-0-有的围绕-32768" class="headerlink" title="同样 16 位，有的围绕 0 有的围绕 32768"></a>同样 16 位，有的围绕 0 有的围绕 32768</h2><p>这批数据两种格式：一种是无符号 16 位（unsigned），数据的载波零点在 32768，数值在 0<del>65535 之间抖；另一种是有符号 16 位（signed），围绕 0 抖，范围是 −32768</del>32767。同样是两个字节 <code>00 80</code>，按无符号读是 32768，按有符号读（小端）是 −32768，完全是两码事。</p><p>读的时候按各自格式来，没问题。问题出在写回。</p><h2 id="减法预处理之后有了负值"><a href="#减法预处理之后有了负值" class="headerlink" title="减法预处理之后有了负值"></a>减法预处理之后有了负值</h2><p>去直流、去背景这类操作本质是减法：估一个背景再从原数据里减掉。减完之后，数据就从「围绕 32768」或者「围绕 0」变成了「围绕 0」，而且必然出现负值（原来低于背景的地方）。</p><p>写回有符号格式（围绕 0）的那种文件，很直接——数据本来就围绕 0，减完还是围绕 0，按有符号 clamp 到 [−32768, 32767] 写进去就行，啥额外操作都不用加。</p><p>但写回无符号格式（围绕 32768）的那种文件就出事了：减法之后围绕 0、带负值，直接当无符号写回去，那些负值会被解释成 65536 附近的超大数，落在一个本该是中灰的位置上全是亮点，整张图就满屏噪点了。</p><h2 id="写回无符号得先平移回它的基准"><a href="#写回无符号得先平移回它的基准" class="headerlink" title="写回无符号得先平移回它的基准"></a>写回无符号得先平移回它的基准</h2><p>所以写回无符号格式之前，得先把数据从「围绕 0」平移回「围绕 32768」——加一个全图统一的直流基准（大概就是整图的均值，约等于 32768），把负值整体抬进 0~65535 的范围，再 clamp：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span> target_unsigned:</span><br><span class="line">    dc_base = <span class="built_in">int</span>(<span class="built_in">round</span>(data.mean()))     <span class="comment"># 全图统一的基准，约 32768</span></span><br><span class="line">    out = data + dc_base                   <span class="comment"># 平移回无符号范围</span></span><br><span class="line">    out = np.clip(out, <span class="number">0</span>, <span class="number">65535</span>).astype(<span class="string">&#x27;&lt;u2&#x27;</span>)</span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line">    out = np.clip(data, -<span class="number">32768</span>, <span class="number">32767</span>).astype(<span class="string">&#x27;&lt;i2&#x27;</span>)   <span class="comment"># 有符号直接 clamp，不加基准</span></span><br></pre></td></tr></table></figure><p>有个反着的坑得强调一下：写回有符号格式<strong>绝对不能</strong>也傻乎乎加个 +32768 的基准。因为数据本来围绕 0，加完 32768 之后落到 32768 附近，再被 <code>(short)</code> 强转，超过 32767 的部分直接溢回 −32768，数据当场写坏。也就是说 clamp 方式必须按目标格式二选一，不能图省事统一一套。</p><p>还有一个细节：那个直流基准必须是<strong>全图统一</strong>的一个值，不能用每一道各自的均值。用各道均值的话，等于把每道都单独拉回自己的 32768，结果是保留了各道之间的直流差——这恰恰是去直流、去背景想抹掉的东西，效果会差很远。这一条是走过弯路之后才纠正过来的：一开始图精确用了各道均值，结果处理完的图还不如不平移干净，换成全图统一直流基准才对。</p><p>说到底，这事的核心就一句：16 位数据的有符号、无符号不是读的时候定死就完了，写回的时候还得按目标格式再判一次。同样是「减完带负值」的中间结果，写回有符号和无符号要走的 clamp 路径完全不同，搞混了就是满屏噪点和数据写坏的区别。</p>]]>
    </content>
    <id>https://cyk.pink/posts/d6c7a14b.html</id>
    <link href="https://cyk.pink/posts/d6c7a14b.html"/>
    <published>2026-07-14T05:25:00.000Z</published>
    <summary>
      <![CDATA[<p>把一批 16 位采样的数据读进来做了点预处理（去直流、去背景这种减法操作），再写回原来的二进制文件给别的专业软件打开。读进来处理的时候一切正常，写回去打开一看，有的文件好好的，有的直接满屏噪点、糊成一团。同样的代码、同样的处理，结果天差地别，卡了好一阵。</p>
<p>根子是 16 位整数到底按有符号还是无符号解读，不是统一的，取决于文件格式。</p>
<h2 id="同样-16-位，有的围绕-0-有的围绕-32768"><a href="#同样-16-位，有的围绕-0-有的围绕-32768" class="headerlink" title="同样 16 位，有的围绕 0 有的围绕 32768"></a>同样 16 位，有的围绕 0 有的围绕 32768</h2><p>这批数据两种格式：一种是无符号 16 位（unsigned），数据的载波零点在 32768，数值在 0<del>65535 之间抖；另一种是有符号 16 位（signed），围绕 0 抖，范围是 −32768</del>32767。同样是两个字节 <code>00 80</code>，按无符号读是]]>
    </summary>
    <title>16 位整数写回文件有符号和无符号要分开 clamp</title>
    <updated>2026-07-14T05:25:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="二进制" scheme="https://cyk.pink/tags/%E4%BA%8C%E8%BF%9B%E5%88%B6/"/>
    <category term="踩坑" scheme="https://cyk.pink/tags/%E8%B8%A9%E5%9D%91/"/>
    <category term="Python" scheme="https://cyk.pink/tags/Python/"/>
    <category term="逆向" scheme="https://cyk.pink/tags/%E9%80%86%E5%90%91/"/>
    <content>
      <![CDATA[<p>手上有一批设备采集下来的二进制数据，没文档，只有几组不同后缀的文件，要把它们读出来画图。全程靠 hex 编辑器一点点猜，记一下逆向这种私有二进制格式的大概路数，顺便记一个踩得很疼的 off-by-one。</p><h2 id="先把头和样点数摸清楚"><a href="#先把头和样点数摸清楚" class="headerlink" title="先把头和样点数摸清楚"></a>先把头和样点数摸清楚</h2><p>这种采集文件，基本结构都是「一个文件头 + 后面一大片采样数据」。第一步是估文件头多大。把文件丢进 hex 编辑器，前面一段往往是固定模式、跟后面的「数据区」长得不一样，头一般是个整数（512、1024、2048 这种）。这批文件摸下来头是 1024 字节。</p><p>样点数通常藏在文件头的某个偏移里。怎么找——算一道简单除法：<code>(文件总大小 - 头) / 每道字节数 = 道数</code>，而 <code>每道字节数 = 样点数 × 每个样点的字节</code>。如果数据是 16 位（2 字节）存的，那去文件头里找一个值，使得「头之后的字节数 &#x2F; 2 &#x2F; 这个值」是个整数、而且这个值本身是合理的样点数（比如 512）。这批文件样点数在头偏移 4 的位置，值是 512。</p><p>到这里，最朴素的读法就有了：跳过 1024 头，后面按 int16 一道一道读：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">nsamp = <span class="number">512</span>           <span class="comment"># 头里偏移4读出来的样点数</span></span><br><span class="line">header = <span class="number">1024</span>         <span class="comment"># 头大小</span></span><br><span class="line">raw = np.frombuffer(data[header:], dtype=<span class="string">&#x27;&lt;i2&#x27;</span>)   <span class="comment"># 小端 int16</span></span><br><span class="line">ntrace = raw.size // nsamp</span><br><span class="line">traces = raw[:ntrace * nsamp].reshape(ntrace, nsamp)</span><br></pre></td></tr></table></figure><h2 id="用-magic-字节区分格式"><a href="#用-magic-字节区分格式" class="headerlink" title="用 magic 字节区分格式"></a>用 magic 字节区分格式</h2><p>事情没这么简单，因为这批数据有四种后缀、对应四种格式，得知道手里这份是哪种。靠后缀判断不可靠（后缀能随便改），靠谱的是文件头里的「魔数」——每个格式在固定位置有一段独特的字节，相当于这个格式的身份证。</p><p>实测下来这批文件：一种开头是 <code>00 07</code>，一种是 <code>56 04 00 00 04 04 00 00</code>，另两种是 <code>01 00</code> 或 <code>00 00</code>。读的时候先嗅探（sniff）一下这几个字节，命中哪种就走哪种的解析器，后缀只当兜底：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">read_auto</span>(<span class="params">path</span>):</span><br><span class="line">    head = <span class="built_in">open</span>(path, <span class="string">&#x27;rb&#x27;</span>).read(<span class="number">8</span>)</span><br><span class="line">    <span class="keyword">if</span> head[:<span class="number">2</span>] == <span class="string">b&#x27;\x00\x07&#x27;</span>:          <span class="keyword">return</span> read_kind_a(path)</span><br><span class="line">    <span class="keyword">if</span> head[:<span class="number">4</span>] == <span class="string">b&#x27;\x56\x04\x00\x00&#x27;</span>:  <span class="keyword">return</span> read_kind_b(path)</span><br><span class="line">    <span class="comment"># 剩下的退回看后缀</span></span><br><span class="line">    ...</span><br></pre></td></tr></table></figure><p>有意思的是，其中三种格式摸到最后发现是同一个套路——都是 <code>[1024 头][int16 数据]</code>，只是 magic 不一样，连样点数偏移都一样（纯属不同厂家巧合撞了布局）。所以这三个的解析逻辑抽到了一个公共函数，各自只是薄薄包一层填自己的 magic。</p><h2 id="一个-off-by-one，数据道数多算一条"><a href="#一个-off-by-one，数据道数多算一条" class="headerlink" title="一个 off-by-one，数据道数多算一条"></a>一个 off-by-one，数据道数多算一条</h2><p>真正折腾人的是第四种。一开始按老套路把它当「<code>[1024 头][int16]</code>」来读，画出来的图第一道总是一根奇怪的直线、后面整体对不齐，怎么调参数都不对。</p><p>后来把它的字节流摊开一格一格看，才发现它根本不是「全局头 + 数据」那种结构，而是<strong>定长记录流</strong>：整个文件就是一条条固定 1028 字节的记录首尾拼接，没有独立的全局头。而且记录还分类型：</p><ul><li>第 0 条是「头记录」，前 4 字节是类型码 <code>1110</code>，后面基本是 0；</li><li>后面才是数据道记录，每条开头是类型码 <code>82</code>，再跟 512 个 int16 样点。</li></ul><p>之前的读法把第 0 条头记录也当成了数据道，所以道数多算了一条、第一道里混进了那条头记录的残值，图自然对不齐。改完之后，先检测开头是不是 <code>1110</code>、是的话跳过这条头记录，真实道数从第 1 条开始数，图一下就正了。</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">read_record_stream</span>(<span class="params">data</span>):</span><br><span class="line">    rec = <span class="number">1028</span></span><br><span class="line">    nrec = <span class="built_in">len</span>(data) // rec</span><br><span class="line">    first = np.frombuffer(data[:<span class="number">4</span>], dtype=<span class="string">&#x27;&lt;i4&#x27;</span>)[<span class="number">0</span>]</span><br><span class="line">    start = <span class="number">1</span> <span class="keyword">if</span> first == <span class="number">1110</span> <span class="keyword">else</span> <span class="number">0</span>      <span class="comment"># 头记录(type=1110)不算数据道</span></span><br><span class="line">    traces = []</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(start, nrec):</span><br><span class="line">        seg = data[i*rec + <span class="number">4</span> : i*rec + <span class="number">4</span> + <span class="number">512</span>*<span class="number">2</span>]   <span class="comment"># 跳过 type 码读 512 样点</span></span><br><span class="line">        traces.append(np.frombuffer(seg, dtype=<span class="string">&#x27;&lt;i2&#x27;</span>))</span><br><span class="line">    <span class="keyword">return</span> np.array(traces)</span><br></pre></td></tr></table></figure><p>逆向这种没文档的二进制格式，路数其实挺固定：先估头大小、再从文件大小反推样点数偏移、用 magic 区分同源的几种格式、剩下就是格格里看字节把记录结构猜出来。最后那个 off-by-one 也提醒一件事——别想当然觉得同一批数据都是一种结构，哪怕是同一个采集软件出的，不同后缀可能用的是完全不同的文件模型，按老套路硬套就会多算一条少算一条。</p>]]>
    </content>
    <id>https://cyk.pink/posts/8b2c5e91.html</id>
    <link href="https://cyk.pink/posts/8b2c5e91.html"/>
    <published>2026-07-13T01:40:00.000Z</published>
    <summary>
      <![CDATA[<p>手上有一批设备采集下来的二进制数据，没文档，只有几组不同后缀的文件，要把它们读出来画图。全程靠 hex 编辑器一点点猜，记一下逆向这种私有二进制格式的大概路数，顺便记一个踩得很疼的 off-by-one。</p>
<h2 id="先把头和样点数摸清楚"><a href="#先把头和样点数摸清楚" class="headerlink" title="先把头和样点数摸清楚"></a>先把头和样点数摸清楚</h2><p>这种采集文件，基本结构都是「一个文件头 + 后面一大片采样数据」。第一步是估文件头多大。把文件丢进 hex 编辑器，前面一段往往是固定模式、跟后面的「数据区」长得不一样，头一般是个整数（512、1024、2048 这种）。这批文件摸下来头是 1024 字节。</p>
<p>样点数通常藏在文件头的某个偏移里。怎么找——算一道简单除法：<code>(文件总大小 - 头) / 每道字节数 = 道数</code>，而 <code>每道字节数 = 样点数 × 每个样点的字节</code>。如果数据是 16 位（2 字节）存的，那去文件头里找一个值，使得「头之后的字节数]]>
    </summary>
    <title>逆向一种私有二进制格式从文件头和 magic 字节说起</title>
    <updated>2026-07-13T01:40:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="踩坑" scheme="https://cyk.pink/tags/%E8%B8%A9%E5%9D%91/"/>
    <category term="雷达" scheme="https://cyk.pink/tags/%E9%9B%B7%E8%BE%BE/"/>
    <category term="信号处理" scheme="https://cyk.pink/tags/%E4%BF%A1%E5%8F%B7%E5%A4%84%E7%90%86/"/>
    <content>
      <![CDATA[<p>雷达数据做完前面的清洗，最后一步往往是加增益（gain）。深部反射看不见、一团黑，加完增益深部就亮起来了。但增益到底在补什么、补多少有讲究，不是无脑把深部拉亮就行。另外有篇单独讲做完增益之后显示色阶的坑，这篇只讲增益算法本身。</p><h2 id="为什么要补：深部比浅部弱太多"><a href="#为什么要补：深部比浅部弱太多" class="headerlink" title="为什么要补：深部比浅部弱太多"></a>为什么要补：深部比浅部弱太多</h2><p>电磁波往地下走，越深衰减越厉害，所以深处的反射回到天线时已经弱了一大截。一道信号画出来，浅部反射很强、深部几乎贴着噪声底，深部想看的目标（比如衬砌后面的脱空）就淹没在里面。增益的作用就是把这个随深度掉下去的幅度，人为往上提一提，让深浅在图上都能看见。</p><h2 id="衰减有两种，数学形式不一样"><a href="#衰减有两种，数学形式不一样" class="headerlink" title="衰减有两种，数学形式不一样"></a>衰减有两种，数学形式不一样</h2><p>深部变弱这件事，根上有两个来源，得分开看。一个是几何扩散——波往外是球面扩散的，能量摊到越来越大的球面上，幅度按幂律（大约 1&#x2F;距离）往下掉。另一个是介电吸收——介质本身把电磁能吃掉一部分，这个是按指数 e^(−α·深度) 掉的。</p><p>关键在于这俩数学形式不同：一个是幂律、一个是指数。所以想补偿，也得用对应的形式去补，硬拿一个套另一个补不齐。</p><h2 id="两种增益对应两种衰减"><a href="#两种增益对应两种衰减" class="headerlink" title="两种增益对应两种衰减"></a>两种增益对应两种衰减</h2><p>增益常见的两种类型正好对应上：power 型按幂律补，对应几何扩散；exp 型按指数补，对应介电吸收。要补哪种衰减就选哪种类型。实际数据里两种衰减同时存在，单用 exp 其实只补了吸收那部分，几何扩散那块没单独补——这正是后面会看到的「补不满」的根因。</p><h2 id="exp-的参数-p-不是拍脑袋"><a href="#exp-的参数-p-不是拍脑袋" class="headerlink" title="exp 的参数 p 不是拍脑袋"></a>exp 的参数 p 不是拍脑袋</h2><p>exp 增益的形式是逐点乘一条指数曲线，关键参数是 p。这里有个容易踩的坑：p 的单位是 1&#x2F;ns，物理上等于 α·v——α 是介质的吸收系数（每米吸收多少），v 是波速（每纳秒走多少米）。所以 p 不是随便给的数，是能从数据反推的：算一下能量随时间的衰减斜率（对数能量随时间的斜率），那基本就是 α·v，也就是该用的 p。实测下来一批数据这个斜率大概在 -0.045&#x2F;ns 附近，用 0.05&#x2F;ns 左右就接近数据本身反推出来的值。</p><h2 id="故意不补满"><a href="#故意不补满" class="headerlink" title="故意不补满"></a>故意不补满</h2><p>这里有个反直觉的点：实测深部相对浅部衰减了大约 18.5dB，但用 exp(0.05) 只补回来约 12dB，还留着约 6dB 没补。这不算错，是故意的。补得太狠，深浅都被拉平，深部强反射和浅部一样亮，反而看不出深浅层次了。判厚度、判脱空这些是要靠相对强弱判断的，全拉平了反倒没法判。所以温和地补、留一点层次，是刻意的。真要完全补平，p 得往 0.077 那边走。</p><p>换句话说，增益是个显示补偿，不是物理反演。它把每道逐点乘同一条 G(t) 曲线（所有道乘同一条），目的是让人眼看得见深部，但不会把衰减掉的能量真的还原回来。增益后的幅值不能拿来当真实幅值定量算。</p><h2 id="保幅就别用-AGC"><a href="#保幅就别用-AGC" class="headerlink" title="保幅就别用 AGC"></a>保幅就别用 AGC</h2><p>还有个 AGC（自动增益控制），它不分深浅物理机制，直接把局部能量拉平归一——原来局部能量跨深度抖得很厉害（标准差好几千），AGC 一压，各处都归到差不多 1，标准差能压到零点零几。看图特别爽，深浅都清楚。但代价是幅值信息全没了：哪里本来强、哪里本来弱，AGC 之后全都拉平，相对强弱看不出来了。</p><p>所以选 exp 还是 AGC，看目的。要保幅、要做定量判读（靠幅值判厚度、判脱空含水），就用 exp，别用 AGC。AGC 只适合纯粹看个图、不在乎幅值大小的场景。</p><h2 id="切类型要重定参数"><a href="#切类型要重定参数" class="headerlink" title="切类型要重定参数"></a>切类型要重定参数</h2><p>最后说个具体的坑：exp 的 p 单位是 1&#x2F;ns，power 的 p 是个无量纲的幂指数，这俩「p」名字一样但含义完全不同。从 exp 切到 power（或反过来），参数不能直接照搬，得按新类型的含义重新定，不然补出来的曲线完全不是想要的那条。</p><p>增益这一步看着简单（就是逐点乘一条曲线），真正要拿捏的是补什么、补多少。补的是随深度掉的幅度，形式得匹配衰减机制（幂律对几何、指数对吸收），力度上故意留一点层次别拉平，保幅用 exp、纯看图才用 AGC。把它当显示补偿、不当物理反演，就不会指望它把深部能量真的还原回来。</p>]]>
    </content>
    <id>https://cyk.pink/posts/9c4e8a2f.html</id>
    <link href="https://cyk.pink/posts/9c4e8a2f.html"/>
    <published>2026-07-12T01:30:00.000Z</published>
    <summary>
      <![CDATA[<p>雷达数据做完前面的清洗，最后一步往往是加增益（gain）。深部反射看不见、一团黑，加完增益深部就亮起来了。但增益到底在补什么、补多少有讲究，不是无脑把深部拉亮就行。另外有篇单独讲做完增益之后显示色阶的坑，这篇只讲增益算法本身。</p>
<h2 id="为什么要补：深部比浅部弱太多"><a href="#为什么要补：深部比浅部弱太多" class="headerlink" title="为什么要补：深部比浅部弱太多"></a>为什么要补：深部比浅部弱太多</h2><p>电磁波往地下走，越深衰减越厉害，所以深处的反射回到天线时已经弱了一大截。一道信号画出来，浅部反射很强、深部几乎贴着噪声底，深部想看的目标（比如衬砌后面的脱空）就淹没在里面。增益的作用就是把这个随深度掉下去的幅度，人为往上提一提，让深浅在图上都能看见。</p>
<h2 id="衰减有两种，数学形式不一样"><a href="#衰减有两种，数学形式不一样" class="headerlink"]]>
    </summary>
    <title>雷达数据做增益到底在补什么</title>
    <updated>2026-07-12T01:30:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="Java" scheme="https://cyk.pink/tags/Java/"/>
    <category term="licence" scheme="https://cyk.pink/tags/licence/"/>
    <category term="安全" scheme="https://cyk.pink/tags/%E5%AE%89%E5%85%A8/"/>
    <category term="防破解" scheme="https://cyk.pink/tags/%E9%98%B2%E7%A0%B4%E8%A7%A3/"/>
    <content>
      <![CDATA[<p>给桌面软件接了一套登录许可证，启动的时候要过一串校验，任一条不过就直接锁应用。licence 防破解的一些具体坑（时间回拨、指纹漂移）之前写过，这里不重复，主要想理一下：一张许可证启动时到底该查哪些项，以及其中几个不那么直观的、容易漏掉的检查。</p><p>整体是个串行链条，前面是许可证本身合不合法，后面是运行环境对不对，最后才是用户名密码。哪一环断了都锁，不是「前面过了后面就随便」。</p><h2 id="许可证本体这几项"><a href="#许可证本体这几项" class="headerlink" title="许可证本体这几项"></a>许可证本体这几项</h2><p>先是把许可证解析出来，格式不对、签名解析失败，直接锁。然后是有效期，过期了锁——但有效期的判断会被改系统时间绕掉，这块得配合本地单调时间记录来防回拨，之前那篇说过的思路，这里不展开。接着是硬件指纹比对，确认现在这台机器就是许可证绑的那台，防一份 licence 拷来拷去到处用。</p><p>这几项是基础，大多数做 licence 的人都会想到。</p><h2 id="版本号得跟软件版本对上"><a href="#版本号得跟软件版本对上" class="headerlink" title="版本号得跟软件版本对上"></a>版本号得跟软件版本对上</h2><p>这条我觉得容易被忽略。许可证里带一个版本号字段，校验的时候不能只看它「有没有」，得看它是不是等于当前软件的版本。</p><p>为什么重要——因为软件会迭代。一份在 v2 时代签发的 licence，里面版本号写的是 2，用户拿着它去跑 v3，如果不校验版本，v3 照样认这份老 licence。这就留下一个口子：买了旧版的人拿着旧 licence 一直白嫖新版本。绑死版本号之后，大版本升级要么重新签发、要么 licence 里显式声明向前兼容哪些版本，主动权在签发方手里。</p><h2 id="注册目录得是当初注册的那个"><a href="#注册目录得是当初注册的那个" class="headerlink" title="注册目录得是当初注册的那个"></a>注册目录得是当初注册的那个</h2><p>这条更隐蔽。注册成功的时候，把「当时所在的安装目录」记进许可证或者本地凭证里。下次启动，拿当前程序的运行目录（<code>user.dir</code>）跟记下来的那个比一下，不一致就锁。</p><p>防的是这种操作：用户在一台机器上注册成功了，然后把整个安装目录连着注册凭证一起打包，拷到另一台机器上跑。光靠硬件指纹的话，如果两台机器硬件凑巧接近、或者指纹本身不够强，可能蒙混过去。但目录是物理路径，拷到别的机器上基本不可能还在同一个绝对路径下，这条等于多加了一层「这确实是当初那台机器、那个位置」的绑定。</p><h2 id="配置校核防的是改配置"><a href="#配置校核防的是改配置" class="headerlink" title="配置校核防的是改配置"></a>配置校核防的是改配置</h2><p>还有个配置校核（proofread）：启动时把关键的本地配置重新算一遍校验和，跟之前存的比对。防的是破解者不去动 licence 本体（那个有签名，动不了），转而去改本地的配置文件——比如把「已激活」标志、剩余时长这些写死成有利值。配置一旦被改，校验和对不上，锁。</p><p>这条的逻辑是：licence 本体有签名护着改不动，那攻击者最可能下手的就是没有签名保护的本地配置，得给这部分也上个完整性校验。</p><h2 id="最后才是用户名密码"><a href="#最后才是用户名密码" class="headerlink" title="最后才是用户名密码"></a>最后才是用户名密码</h2><p>所有上面的环境校验都过了，才走到用户名密码这一步。它放在最后是有讲究的：用户名密码防的是「会操作电脑但不会逆向」的人，真正能破前面那些链路的人，根本不需要猜密码。所以把它放最前面没意义，先确认环境和许可证没问题，最后再用密码挡一道普通使用者层面的越权。</p><p>这一串串起来，思路其实就是「licence 本体靠签名、运行环境靠多维度绑定」。签名保证许可证没被改，版本号、目录、配置校核把「拿合法 licence 换个环境用」这条路一层层堵上，最后用密码挡住普通越权。哪条单独拎出来都不绝对，但叠成一个链条之后，想绕过去得同时把好几处都对上，门槛就上去了。</p>]]>
    </content>
    <id>https://cyk.pink/posts/3f7d9c2b.html</id>
    <link href="https://cyk.pink/posts/3f7d9c2b.html"/>
    <published>2026-07-11T07:55:00.000Z</published>
    <summary>
      <![CDATA[<p>给桌面软件接了一套登录许可证，启动的时候要过一串校验，任一条不过就直接锁应用。licence 防破解的一些具体坑（时间回拨、指纹漂移）之前写过，这里不重复，主要想理一下：一张许可证启动时到底该查哪些项，以及其中几个不那么直观的、容易漏掉的检查。</p>
<p>整体是个串行链条，前面是许可证本身合不合法，后面是运行环境对不对，最后才是用户名密码。哪一环断了都锁，不是「前面过了后面就随便」。</p>
<h2 id="许可证本体这几项"><a href="#许可证本体这几项" class="headerlink" title="许可证本体这几项"></a>许可证本体这几项</h2><p>先是把许可证解析出来，格式不对、签名解析失败，直接锁。然后是有效期，过期了锁——但有效期的判断会被改系统时间绕掉，这块得配合本地单调时间记录来防回拨，之前那篇说过的思路，这里不展开。接着是硬件指纹比对，确认现在这台机器就是许可证绑的那台，防一份 licence 拷来拷去到处用。</p>
<p>这几项是基础，大多数做 licence 的人都会想到。</p>
<h2 id="版本号得跟软件版本对上"><a]]>
    </summary>
    <title>一张登录许可证启动时到底该查哪些</title>
    <updated>2026-07-11T07:55:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="Java" scheme="https://cyk.pink/tags/Java/"/>
    <category term="licence" scheme="https://cyk.pink/tags/licence/"/>
    <category term="安全" scheme="https://cyk.pink/tags/%E5%AE%89%E5%85%A8/"/>
    <category term="防破解" scheme="https://cyk.pink/tags/%E9%98%B2%E7%A0%B4%E8%A7%A3/"/>
    <content>
      <![CDATA[<p>做了个注册机给桌面软件签发许可证，注册机本身有个管理员解锁密码——不是最终用户的密码，是防止谁都能进来乱签 license 的那道门。一开始图省事，密码明文写在 <code>gradle.properties</code> 里，程序运行的时候读出来比对。能用是能用，但越想越不对劲。</p><p>问题在于，这个密码随包一起发出去了。<code>gradle.properties</code> 里的值，打包完之后会以某种形式落到产物里，最差就是直接生成一个 <code>register.properties</code> 跟着 exe 走。破解者拿到发版包，翻一翻配置文件，明文密码就在那儿躺着，这道门等于没上锁。</p><p>后来改成这样：密码在打包阶段就转成 MD5，然后通过 exe4j 的 VM 参数固化进 exe，运行时直接从系统属性里读 MD5 来比对，全程不落外部配置文件。</p><p>打包脚本里大致是：</p><figure class="highlight groovy"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 打包时把明文密码算成 MD5，塞进 exe4j 的 VM 参数</span></span><br><span class="line"><span class="keyword">def</span> pwdMd5 = md5(project.property(<span class="string">&#x27;registerPassword&#x27;</span>))</span><br><span class="line"><span class="keyword">def</span> vmArgs = <span class="string">&quot;-Dregister.password.md5=$&#123;pwdMd5&#125;&quot;</span></span><br><span class="line"><span class="comment">// 这段 vmArgs 写进 exe4j 配置，随 exe 一起固化</span></span><br></pre></td></tr></table></figure><p>运行时校验这边，按优先级找这个 MD5：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> String <span class="title function_">resolveExpectedPasswordMd5</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 1. 打包时通过 -Dregister.password.md5 注入的（生产）</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">md5</span> <span class="operator">=</span> System.getProperty(<span class="string">&quot;register.password.md5&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (StringUtils.isNotEmpty(md5)) &#123;</span><br><span class="line">        <span class="keyword">return</span> md5;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 2. 开发态 IDEA 直接跑，读 properties 里的明文现算（仅本机）</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">pwd</span> <span class="operator">=</span> loadDevProperties().getProperty(<span class="string">&quot;registerPassword&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (StringUtils.isNotEmpty(pwd)) &#123;</span><br><span class="line">        <span class="keyword">return</span> md5(pwd);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 3. 兜底</span></span><br><span class="line">    <span class="keyword">return</span> md5(FALLBACK_PASSWORD);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>用户输入密码，同样 <code>md5</code> 一遍，跟期望值比，对得上才放行。全程只存和比 MD5，明文密码不进运行态、不落盘。</p><p>这么改之后几个好处。一是发版包里翻不到明文密码了，最起码「翻配置文件」这条路堵死了，想拿原值得反 exe。二是 VM 参数 <code>-D</code> 是打进 exe4j 配置、随 exe 固化的，不是旁边带个文件，没那么容易被一眼发现和替换。三是开发态不受影响——IDEA 里直接跑 main 的时候没有那个 VM 参数，就走第二条读 properties 现算，本机调试照样方便。</p><p>当然这不是什么高强度的防护，MD5 本身早不该直接用来存密码（正经得加盐、用慢哈希），注册机这个场景门槛要求没那么高，这么搞够用。真要紧的是别把明文密码跟包一起发出去——那不叫加密，那叫附赠钥匙。</p>]]>
    </content>
    <id>https://cyk.pink/posts/c4e2b81a.html</id>
    <link href="https://cyk.pink/posts/c4e2b81a.html"/>
    <published>2026-07-10T03:08:00.000Z</published>
    <summary>
      <![CDATA[<p>做了个注册机给桌面软件签发许可证，注册机本身有个管理员解锁密码——不是最终用户的密码，是防止谁都能进来乱签 license 的那道门。一开始图省事，密码明文写在 <code>gradle.properties</code> 里，程序运行的时候读出来比对。能用是能用，但越想越不对劲。</p>
<p>问题在于，这个密码随包一起发出去了。<code>gradle.properties</code> 里的值，打包完之后会以某种形式落到产物里，最差就是直接生成一个 <code>register.properties</code> 跟着 exe 走。破解者拿到发版包，翻一翻配置文件，明文密码就在那儿躺着，这道门等于没上锁。</p>
<p>后来改成这样：密码在打包阶段就转成 MD5，然后通过 exe4j 的 VM 参数固化进 exe，运行时直接从系统属性里读 MD5 来比对，全程不落外部配置文件。</p>
<p>打包脚本里大致是：</p>
<figure class="highlight groovy"><table><tr><td class="code"><pre><span]]>
    </summary>
    <title>注册机的管理员密码别明文写在配置文件里</title>
    <updated>2026-07-10T03:08:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="踩坑" scheme="https://cyk.pink/tags/%E8%B8%A9%E5%9D%91/"/>
    <category term="JavaFX" scheme="https://cyk.pink/tags/JavaFX/"/>
    <category term="打包" scheme="https://cyk.pink/tags/%E6%89%93%E5%8C%85/"/>
    <content>
      <![CDATA[<p>用 exe4j 把一个 JavaFX 桌面程序打成 exe，自带 JRE 一起发出去，结果双击 exe 直接弹「找不到 jvm」之类的错。机器上明明装了 Java，自带的 JRE 目录也都在，就是起不来。</p><p>折腾一圈发现是 exe4j 找 JRE 的路径没配对。</p><p>exe4j 打出来的 exe 本质是个启动器，它自己不包含 JVM，运行的时候要先在机器上找一个可用的 JRE 再拉起真正的程序。它找 JRE 是按一个「搜索顺序」来的，默认会先去找系统注册的 Java、<code>JAVA_HOME</code> 之类，找不到才往下走。问题就出在这：开发机上系统 Java 是高版本的，能蒙混过去；到了用户那台干净机器上，啥系统 Java 都没有，它又没被告诉「去我自带的那个 .&#x2F;jre 里找」，自然就报找不到。</p><p>修法是在 exe4j 的配置里显式给一个搜索路径，优先指向打包进去的那个 JRE 目录。exe4j 的配置是份 xml，对应的是 <code>&lt;searchSequence&gt;</code> 这一段：</p><figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">searchSequence</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">directory</span> <span class="attr">location</span>=<span class="string">&quot;./jre&quot;</span> /&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">searchSequence</span>&gt;</span></span><br></pre></td></tr></table></figure><p>就是把「相对于 exe 所在目录的 <code>./jre</code>」放到搜索序列的最前面。打包的时候把精简过的 JRE（我用 jlink 裁了一份只含要用的模块的）整个放到发版目录的 <code>jre/</code> 下，exe 起来就先从这儿找，稳稳命中自带的那份，跟用户机器上装没装 Java 彻底脱钩。</p><p>有个坑顺带说一下：这个 JRE 目录名得和配置里写的对得上，打包脚本挪文件的时候别把 <code>jre</code> 改名或者放到别的层级，否则 <code>./jre</code> 这个相对路径就指空了，又是同样找不到的报错。我用 Gradle task 自动生成这份 exe4j 配置 + 拷 JRE，两步用同一个目录变量串起来，省得手抖对不上。</p><p>其实这类「在我这好好的、到你那不行」的问题，根子基本都是把开发机上的环境当成了默认环境。启动器找 JRE 这种事，别指望它自己去猜，显式把自带 JRE 的路径塞到搜索序列最前面，一份自包含的 exe 才算真的能到处跑。</p>]]>
    </content>
    <id>https://cyk.pink/posts/9f3a7c2e.html</id>
    <link href="https://cyk.pink/posts/9f3a7c2e.html"/>
    <published>2026-07-09T08:20:00.000Z</published>
    <summary>
      <![CDATA[<p>用 exe4j 把一个 JavaFX 桌面程序打成 exe，自带 JRE 一起发出去，结果双击 exe 直接弹「找不到 jvm」之类的错。机器上明明装了 Java，自带的 JRE 目录也都在，就是起不来。</p>
<p>折腾一圈发现是 exe4j 找 JRE 的路径没配对。</p>
<p>exe4j 打出来的 exe 本质是个启动器，它自己不包含 JVM，运行的时候要先在机器上找一个可用的 JRE 再拉起真正的程序。它找 JRE 是按一个「搜索顺序」来的，默认会先去找系统注册的 Java、<code>JAVA_HOME</code> 之类，找不到才往下走。问题就出在这：开发机上系统 Java 是高版本的，能蒙混过去；到了用户那台干净机器上，啥系统 Java 都没有，它又没被告诉「去我自带的那个 .&#x2F;jre 里找」，自然就报找不到。</p>
<p>修法是在 exe4j 的配置里显式给一个搜索路径，优先指向打包进去的那个 JRE 目录。exe4j 的配置是份 xml，对应的是 <code>&lt;searchSequence&gt;</code>]]>
    </summary>
    <title>exe4j 打包出来的 exe 报找不到 jvm</title>
    <updated>2026-07-09T08:20:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="GPR" scheme="https://cyk.pink/tags/GPR/"/>
    <category term="雷达" scheme="https://cyk.pink/tags/%E9%9B%B7%E8%BE%BE/"/>
    <category term="信号处理" scheme="https://cyk.pink/tags/%E4%BF%A1%E5%8F%B7%E5%A4%84%E7%90%86/"/>
    <category term="背景去除" scheme="https://cyk.pink/tags/%E8%83%8C%E6%99%AF%E5%8E%BB%E9%99%A4/"/>
    <content>
      <![CDATA[<p>整张 GPR 剖面上常有一层「每道都长一样」的横向条带，像照片蒙了灰雾。擦掉它，局部目标才显出来。</p><h2 id="从哪来"><a href="#从哪来" class="headerlink" title="从哪来"></a>从哪来</h2><p>有些成分在全测线每一道都几乎相同：</p><ul><li>直达波：发射天线直接窜到接收天线，每道开头都有；</li><li>地表或天线面的反射、耦合响应；</li><li>大范围的均匀层状反射。</li></ul><p>它们在每个位置出现的时间和强度都差不多，画出来就是水平横条，盖住局部异常。</p><h2 id="平均道就是背景"><a href="#平均道就是背景" class="headerlink" title="平均道就是背景"></a>平均道就是背景</h2><p>对每个时间样点，把所有道的值求平均，得到一条「平均道」。水平背景在每道都一样，在平均里相干保留；局部目标位置相位不同，互相抵消。</p><p>从每道减去这条平均道，背景没了，局部目标活下来。</p><p>统一公式：</p><p>$$D_{out}[t,x]&#x3D;D[t,x]-\alpha\cdot\hat B[t,x],\qquad \alpha\in[0,1]$$</p><p>$\alpha&#x3D;1$ 全减；$\alpha&lt;1$ 部分减，适合既看点目标又想看连续界面的情况。</p><h2 id="背景估计的几种方法"><a href="#背景估计的几种方法" class="headerlink" title="背景估计的几种方法"></a>背景估计的几种方法</h2><ul><li>全局 mean&#x2F;median：所有道一起求均值或中位数。</li><li>滑窗 mean&#x2F;median：只取当前道附近窗口内的道求平均，适合背景沿测线缓慢变化。</li><li>SVD：取数据矩阵前几个奇异图，背景通常低秩。</li><li>f-k 滤波：在二维频谱里把低空间波数成分当背景去掉。</li></ul><h2 id="mean-还是-median？"><a href="#mean-还是-median？" class="headerlink" title="mean 还是 median？"></a>mean 还是 median？</h2><p>一个 3×4 的迷你 B-scan：第 0 行是直达波（每道都是 50），第 1 行第 3 道有一根钢筋（30）：</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">            道1  道2  道3  道4</span><br><span class="line">行0(直达波)  50   50   50   50</span><br><span class="line">行1(有钢筋)   2   30    3    1</span><br></pre></td></tr></table></figure><p>用 mean：行 1 平均被钢筋拉到 9，减完后其他道出现 −7&#x2F;−8 的凹坑。</p><p>用 median：行 1 中位数是 2.5，减完后钢筋 +27.5，其他道几乎不受影响。</p><p>有孤立强目标时，优先用 median。</p><h2 id="最大的陷阱"><a href="#最大的陷阱" class="headerlink" title="最大的陷阱"></a>最大的陷阱</h2><p>如果你要找的目标本身就是连续水平界面，比如层间脱空、连续裂隙，它在每道都出现，也会被当成背景减掉。</p><ul><li>找钢筋、管线这种点目标：背景去除是好工具；</li><li>找脱空、连续裂隙这种面目标：关掉背景去除，或者把窗调得很大。</li></ul><p>判读要点：顶部直达波亮纹变淡、局部双曲线更清楚，就对了。同时要警惕横向连续反射有没有莫名消失。</p>]]>
    </content>
    <id>https://cyk.pink/posts/3440ab25.html</id>
    <link href="https://cyk.pink/posts/3440ab25.html"/>
    <published>2026-07-08T02:00:00.000Z</published>
    <summary>
      <![CDATA[<p>整张 GPR 剖面上常有一层「每道都长一样」的横向条带，像照片蒙了灰雾。擦掉它，局部目标才显出来。</p>
<h2 id="从哪来"><a href="#从哪来" class="headerlink" title="从哪来"></a>从哪来</h2><p>有些成分在全测线每一道都几乎相同：</p>
<ul>
<li>直达波：发射天线直接窜到接收天线，每道开头都有；</li>
<li>地表或天线面的反射、耦合响应；</li>
<li>大范围的均匀层状反射。</li>
</ul>
<p>它们在每个位置出现的时间和强度都差不多，画出来就是水平横条，盖住局部异常。</p>
<h2 id="平均道就是背景"><a href="#平均道就是背景" class="headerlink"]]>
    </summary>
    <title>GPR 背景去除：擦掉公共水平条带</title>
    <updated>2026-07-08T02:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="踩坑" scheme="https://cyk.pink/tags/%E8%B8%A9%E5%9D%91/"/>
    <category term="雷达" scheme="https://cyk.pink/tags/%E9%9B%B7%E8%BE%BE/"/>
    <category term="信号处理" scheme="https://cyk.pink/tags/%E4%BF%A1%E5%8F%B7%E5%A4%84%E7%90%86/"/>
    <content>
      <![CDATA[<p>探地雷达的剖面图（B-scan）上，经常有一些横向几乎不变的条带或底色——直达波、天线环回响、系统本底。它们跟具体的地下反射没关系，却往往占了大头，把真正想看的弱反射压在下面看不清。背景去除就是想办法把这些共模背景估出来再减掉。</p><p>先跟 dewow 划清界限：dewow 沿时间轴（每道内部）去道自己的低频漂移；背景去除沿<strong>空间轴</strong>（道与道之间）去横向的共模背景。两者方向正交。统一写成一个公式就是 <code>D − α·B̂</code>——估一个背景 B̂，乘个强度系数 α 再减掉，alpha 默认 1.0 就是全减。</p><p>难点不在「减」，在「背景 B̂ 怎么估才估得准」。试下来几种路子各有擅长的场景。</p><h2 id="mean-median：沿线平均，最朴素"><a href="#mean-median：沿线平均，最朴素" class="headerlink" title="mean &#x2F; median：沿线平均，最朴素"></a>mean &#x2F; median：沿线平均，最朴素</h2><p>最直接的是沿空间方向（跨道）求平均当背景。直达波这种几乎每道都一样的东西，一平均就冒出来了，减掉很干净。把均值换成中位数（median）就是 median 法，好处是抗异常——少数几道的强反射不会把整体背景带偏。</p><p>这一类对「横向基本恒定」的背景效果好。但雷达数据里的背景不总是规规矩矩水平的，有时候是斜的（剖面沿线有倾角）、有时候是周期性振铃（ringing），mean 沿线一平均，倾斜的条带平均不掉、振铃也对付不了，这时候就得换家伙。</p><h2 id="SVD-低秩：背景本质是低秩的"><a href="#SVD-低秩：背景本质是低秩的" class="headerlink" title="SVD 低秩：背景本质是低秩的"></a>SVD 低秩：背景本质是低秩的</h2><p>换一个角度看：一条横向恒定的背景，在「道×时间」这个矩阵里其实就是秩为 1 的成分（每一道都差一个常数）。倾斜的条带也一样，本质还是秩 1，只是均值法没顺着它的斜路去减。</p><p>SVD 法就是利用这个——对数据矩阵做奇异值分解，最大那几个奇异值对应的，往往就是这些占大头的背景结构。砍掉前 k 个主分量（比如 k&#x3D;1），用剩下的奇异值重构，背景就没了：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">_svd_background</span>(<span class="params">data, k=<span class="number">1</span></span>):</span><br><span class="line">    U, S, Vt = np.linalg.svd(data, full_matrices=<span class="literal">False</span>)</span><br><span class="line">    S[:k] = <span class="number">0</span>                      <span class="comment"># 砍掉前 k 个最大奇异值</span></span><br><span class="line">    <span class="keyword">return</span> (U * S) @ Vt            <span class="comment"># 重构 = 背景</span></span><br></pre></td></tr></table></figure><p>它的好处是数据驱动，不假设背景水平——倾斜条带、振铃这种 mean 减不动的，SVD 砍掉对应的秩就干净了。代价是要算一次 SVD，数据大的时候比 mean 慢，工程上可以降到 float32 省时间。</p><h2 id="f-k-频域：压掉低空间波数"><a href="#f-k-频域：压掉低空间波数" class="headerlink" title="f-k 频域：压掉低空间波数"></a>f-k 频域：压掉低空间波数</h2><p>还有一招是变换到频率-波数域（f-k 域）处理。思路是：横向缓变的背景，在空间频率（波数 kx）上表现为低波数成分——波长很长、变化很慢。反过来，局部反射是高波数。所以在 f-k 域把低空间波数（|kx| 小于某个截止 fk_kcut）的成分抠出来当背景减掉，留下的就是局部变化：</p><p>截止 fk_kcut 的单位是「每道多少个周期」，默认取 0.02 左右，大致意思是波长超过 50 道的才算背景。这招对横向恒定的背景特别彻底，残差能压到很小，同时局部凸起（真正的反射）原样留下。它跟 SVD 都是「空间低通当背景、减完得空间高通」的思路，f-k 是频域的砖墙低通，更锐利。</p><h2 id="alpha：留一手护住连续界面"><a href="#alpha：留一手护住连续界面" class="headerlink" title="alpha：留一手护住连续界面"></a>alpha：留一手护住连续界面</h2><p>有个旋钮 alpha 得单独说，它控制减多少。默认 1.0 是全减，但有时候不能减这么狠。雷达里有一类东西是「连续的水平界面」——层间脱空、连续裂隙，它们本身就是横向延展的，长得跟背景很像。背景去除开满，会把这种连续界面也当背景一起抹掉，该看到的病害反而没了。</p><p>碰到这种情况就把 alpha 调小（部分减除），或者把窗开大、甚至关掉这一步。经验上是：要同时看钢筋点状反射和脱空这种连续界面，alpha 不能给到 1，得留一部分背景护住连续界面。这也是背景去除最容易翻车的地方——减得太干净，连该留下的东西一起干净了。</p><h2 id="怎么选"><a href="#怎么选" class="headerlink" title="怎么选"></a>怎么选</h2><p>水平静态背景（直达波最典型）→ mean 就够，又快又简单；剖面有倾角、有振铃 → 上 SVD；横向强缓变背景、想干净留局部凸起 → f-k；碰到连续水平界面型病害 → alpha 调小或跳过这一步。位置上，背景去除放在 dewow 和带通之后、增益之前。</p><p>背景去除的核心其实就一句话：背景和反射在「空间方向上的变化快慢」上不一样，背景慢、反射快，想方设法把慢的估出来减掉就行。mean 是最朴素的估法，SVD 和 f-k 是更聪明的估法，alpha 是防误伤的保险——选哪个，看背景长什么样、有没有不能误伤的连续界面。</p>]]>
    </content>
    <id>https://cyk.pink/posts/1e9f4c6b.html</id>
    <link href="https://cyk.pink/posts/1e9f4c6b.html"/>
    <published>2026-07-07T06:00:00.000Z</published>
    <summary>
      <![CDATA[<p>探地雷达的剖面图（B-scan）上，经常有一些横向几乎不变的条带或底色——直达波、天线环回响、系统本底。它们跟具体的地下反射没关系，却往往占了大头，把真正想看的弱反射压在下面看不清。背景去除就是想办法把这些共模背景估出来再减掉。</p>
<p>先跟 dewow 划清界限：dewow 沿时间轴（每道内部）去道自己的低频漂移；背景去除沿<strong>空间轴</strong>（道与道之间）去横向的共模背景。两者方向正交。统一写成一个公式就是 <code>D − α·B̂</code>——估一个背景 B̂，乘个强度系数 α 再减掉，alpha 默认 1.0 就是全减。</p>
<p>难点不在「减」，在「背景 B̂ 怎么估才估得准」。试下来几种路子各有擅长的场景。</p>
<h2 id="mean-median：沿线平均，最朴素"><a href="#mean-median：沿线平均，最朴素" class="headerlink" title="mean &#x2F; median：沿线平均，最朴素"></a>mean &#x2F;]]>
    </summary>
    <title>雷达剖面背景去除的几种做法</title>
    <updated>2026-07-07T06:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="licence" scheme="https://cyk.pink/tags/licence/"/>
    <category term="安全" scheme="https://cyk.pink/tags/%E5%AE%89%E5%85%A8/"/>
    <category term="防破解" scheme="https://cyk.pink/tags/%E9%98%B2%E7%A0%B4%E8%A7%A3/"/>
    <content>
      <![CDATA[<p>前面写过一版防时间回拨，思路是本地存一个「上次见过的时间」，下次启动发现现在比它还早，就判定时间被调过。那招够用但比较朴素，只能挡住随手调表的人。后来又琢磨了一圈，离线环境里想认真防「改系统时间续命」，其实有几条不同路数的思路，单独哪一条都有破法，得叠着用。</p><p>先说个大前提：完全离线、又不能联网对时的环境下，这是软件防破解里的经典难题，没有银弹。下面这些都是抬门槛，不是绝对防住。</p><h2 id="别看「现在几点」，看「过去留的痕」"><a href="#别看「现在几点」，看「过去留的痕」" class="headerlink" title="别看「现在几点」，看「过去留的痕」"></a>别看「现在几点」，看「过去留的痕」</h2><p>第一类思路是文件时间戳审计。软件运行的时候，往系统各处（日志、临时目录、配置文件深处、注册表角落）偷偷写一些文件。每次启动去读这些文件的最后修改时间，只要发现当前系统时间比某个历史文件的修改时间还早——说明时间被往回调了，直接锁死。逻辑是：正常情况下，现在不可能比过去还早。</p><p>这招成本最低，但文件能被删，删干净了就重置，所以只能当一道初筛。</p><h2 id="时间能倒退，运行时长不能"><a href="#时间能倒退，运行时长不能" class="headerlink" title="时间能倒退，运行时长不能"></a>时间能倒退，运行时长不能</h2><p>第二类是单向计数器，我觉得是更靠谱的方向。墙上的时钟能调，但「软件实际跑了多久」这件事，用户没法自然地把它调回去。</p><p>具体两种做法。一种是限制使用次数：许可证带一个最大次数，每次启动在本地加密文件里扣一次，用完就锁。另一种是累计运行时间：后台开个线程，软件每跑一段时间，就在本地累计一下。然后拿这个累计时长去跟系统时间流逝的量对账——系统时间说只过了一小时，可我记着的累计运行时长涨了十小时，那中间一定被调过表。</p><p>对账用的「实际跑了多久」不能用 <code>System.currentTimeMillis()</code>，那个跟着系统时钟走，一调表就废。得用单调时钟，Java 里就是 <code>System.nanoTime()</code>。它不受墙上调时间影响，只反映真实流逝的纳秒：</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 后台线程周期性累计真实运行时长（nanoTime 单调，不受改系统时间影响）</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">volatile</span> <span class="type">long</span> accumulatedNanos;</span><br><span class="line"><span class="keyword">private</span> <span class="type">long</span> <span class="variable">segmentStart</span> <span class="operator">=</span> System.nanoTime();</span><br><span class="line"><span class="keyword">while</span> (running) &#123;</span><br><span class="line">    <span class="type">long</span> <span class="variable">elapsed</span> <span class="operator">=</span> System.nanoTime() - segmentStart;</span><br><span class="line">    accumulatedNanos += elapsed;</span><br><span class="line">    persistAccumulated(accumulatedNanos);   <span class="comment">// 加密落盘</span></span><br><span class="line">    segmentStart = System.nanoTime();</span><br><span class="line">    Thread.sleep(INTERVAL);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>启动时把「系统时间流逝」和「nanoTime 累计」一对，差得离谱（比如系统时间几乎没走、累计时长却涨了一大截，或者反过来系统时间大跳而累计没动）就认定被动了手脚，锁应用。这比单纯记墙上时间硬，因为它不依赖「时间只往前走」这个假设，而是直接量「真实运行量」。</p><h2 id="借别人频繁更新的时钟对账"><a href="#借别人频繁更新的时钟对账" class="headerlink" title="借别人频繁更新的时钟对账"></a>借别人频繁更新的时钟对账</h2><p>第三类是跨应用对账，专门对付那种「运行时间很短、关了再开」的工具。你的软件可能就跑几秒，累计时长根本攒不起来，用户大可以「跑五秒、关掉、调表、再跑五秒」地白嫖。</p><p>这种就别指望自己的运行时长了，借系统里那些天天在跑的东西：Windows 读系统事件日志里最近一条事件的时间，Linux&#x2F;macOS 看 <code>~/.bash_history</code>、<code>/var/log</code> 下最新文件的时间。这些都是别的程序留下的时间脚印，你启动的时候如果发现系统时间比这些脚印还早，那系统时间肯定被倒拨了。</p><h2 id="硬件单调时钟和加密狗"><a href="#硬件单调时钟和加密狗" class="headerlink" title="硬件单调时钟和加密狗"></a>硬件单调时钟和加密狗</h2><p>再往上还有硬件层的招。就算改了墙上时间，有些底层时钟是调不回去的：Windows 的 <code>GetTickCount64</code>、Linux 的 <code>clock_gettime(CLOCK_MONOTONIC)</code>，表示本次开机以来流逝的毫秒，跟系统时钟脱钩。每次退出前把这次的 monotonic 时间累加存下来，能挡「开机时长」被作弊。再往上就是带独立时钟芯片的 USB 加密狗了，软件只向狗要时间，改系统时间完全没用，那是高价值软件才上得起的配置。</p><h2 id="计数器存哪儿才不被删"><a href="#计数器存哪儿才不被删" class="headerlink" title="计数器存哪儿才不被删"></a>计数器存哪儿才不被删</h2><p>上面这些都绕不开一个问题：计数器、累计时长存在哪，才不会被用户直接删掉重置。删了文件、软件以为是第一次运行，前面全白干。实务里是散着存：一部分放应用自己的数据目录（放正经的签名许可证文件），一部分加密后伪装成系统文件丢到临时目录的角落，Windows 还可以塞注册表深处。不指望哪一处绝对安全，而是让用户没法一眼找全、删干净。</p><p>兜底还有个体验问题：主板电池没电、跨时区出差、系统自动对时，都可能让时间无意中跳一下，一刀切锁死会招来客诉。所以一般都留个宽限期，差得小的当时区抖动记下来警告但不锁，差得大的才真锁。</p><p>这几条叠下来，核心其实就是把信任从「系统能不能给个准时间」换成了「用户改不了的那些量」——真实运行时长、别人的时间脚印、硬件单调时钟。单拎一个都有破法，叠在一起，普通用户基本没那个耐心去一个个找全删干净。</p>]]>
    </content>
    <id>https://cyk.pink/posts/2e8c4d61.html</id>
    <link href="https://cyk.pink/posts/2e8c4d61.html"/>
    <published>2026-07-06T06:30:00.000Z</published>
    <summary>
      <![CDATA[<p>前面写过一版防时间回拨，思路是本地存一个「上次见过的时间」，下次启动发现现在比它还早，就判定时间被调过。那招够用但比较朴素，只能挡住随手调表的人。后来又琢磨了一圈，离线环境里想认真防「改系统时间续命」，其实有几条不同路数的思路，单独哪一条都有破法，得叠着用。</p>
<p>先说个大前提：完全离线、又不能联网对时的环境下，这是软件防破解里的经典难题，没有银弹。下面这些都是抬门槛，不是绝对防住。</p>
<h2 id="别看「现在几点」，看「过去留的痕」"><a href="#别看「现在几点」，看「过去留的痕」" class="headerlink"]]>
    </summary>
    <title>离线软件怎么防用户改系统时间绕过到期</title>
    <updated>2026-07-06T06:30:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="踩坑" scheme="https://cyk.pink/tags/%E8%B8%A9%E5%9D%91/"/>
    <category term="雷达" scheme="https://cyk.pink/tags/%E9%9B%B7%E8%BE%BE/"/>
    <category term="信号处理" scheme="https://cyk.pink/tags/%E4%BF%A1%E5%8F%B7%E5%A4%84%E7%90%86/"/>
    <content>
      <![CDATA[<p>处理探地雷达（GPR）数据，拿来第一步基本都先做 dewow。刚接触的时候光看名字不知道它在去什么，后来弄明白了，记一下。</p><p>先说两个词。雷达每次发射接收，得到的是一条沿时间方向的采样序列，叫一道（A-scan）；把很多道沿测线方向排开，就成了二维的剖面图（B-scan）。dewow 处理的是「一道」内部的事。</p><h2 id="一道信号本来该围绕-0，实际常常整体漂"><a href="#一道信号本来该围绕-0，实际常常整体漂" class="headerlink" title="一道信号本来该围绕 0，实际常常整体漂"></a>一道信号本来该围绕 0，实际常常整体漂</h2><p>理论上，去掉直达波以后，一道雷达信号应该围绕 0 这条基线上下抖动——有反射的地方抖一下，没反射的地方贴着 0。实际拿到的数据不是这样，整条信号会慢慢往上拱一段、又往下沉一段，整体不贴着 0，像是在一个缓慢起伏的包络上叠加了高频反射。这个慢漂就叫 wow。</p><p>wow 这个词是早期音响那边的行话，本来指唱片转速不稳导致的声音低频飘忽，借过来形容信号的低频基线漂移。它的来源一般是天线跟地面的耦合、仪器本底、线缆感应这些，跟真正的地下反射没关系，但它占着基线、带着整道信号跑偏。</p><p>为什么要先去掉它——基线不归零，后面一堆处理都受拖累。算能量、判反射强度的时候，慢漂会垫一层虚假的底；显示的时候，整道压在直流偏置上，弱反射更看不清。</p><h2 id="沿时间方向算个滑动平均，再减掉"><a href="#沿时间方向算个滑动平均，再减掉" class="headerlink" title="沿时间方向算个滑动平均，再减掉"></a>沿时间方向算个滑动平均，再减掉</h2><p>做法很直白：沿时间方向（每道内部）做一个滑动平均，得到那条慢漂的包络线，然后从原信号里把它减掉：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">dewow</span>(<span class="params">data, window=<span class="number">101</span></span>):</span><br><span class="line">    trend = boxcar_mean(data, window, axis=<span class="number">0</span>)   <span class="comment"># 沿时间轴(axis=0)的滑动平均</span></span><br><span class="line">    <span class="keyword">return</span> data - trend</span><br></pre></td></tr></table></figure><p>滑动平均是个低通，原信号减掉低通剩下的就是高频——所以 dewow 本质上是个高通滤波，把低频漂移滤掉，留下相对高频的反射。这一步的代码其实就一行减法，真正要拿捏的是那个 window（窗长）。</p><h2 id="窗长就是截止频率，太小会把真信号也削了"><a href="#窗长就是截止频率，太小会把真信号也削了" class="headerlink" title="窗长就是截止频率，太小会把真信号也削了"></a>窗长就是截止频率，太小会把真信号也削了</h2><p>窗长直接决定去多「低」的频。窗越长，滑动平均越平滑、跟得上的漂移越慢，等于截止频率越低，只去更低的频；窗越短，截止频率越高，连稍微低一点的有用反射都当漂移给减了。</p><p>这是个要小心的地方：窗太小，会把真实的低频目标（比如深部的大块反射）当成 wow 削掉，深部信息受损；低频天线、探测深部大目标的数据，dewow 不能开太狠。默认一百出头个样点是个常用起点，具体得看数据的时间窗和天线主频调。</p><h2 id="这一步是沿时间轴，跟背景去除别搞混"><a href="#这一步是沿时间轴，跟背景去除别搞混" class="headerlink" title="这一步是沿时间轴，跟背景去除别搞混"></a>这一步是沿时间轴，跟背景去除别搞混</h2><p>有个特别容易混的点：dewow 是沿<strong>时间轴</strong>（每道内部，axis&#x3D;0）去每道自己的低频漂移。而后面要做的「背景去除」是沿<strong>空间轴</strong>（道与道之间，axis&#x3D;1）去横向的共模背景。两个方向正交，处理的东西完全不同——一个是道内的慢漂，一个是跨道的条带。光看「都是减一个平均」容易以为是一回事，实际上轴不一样，减出来的效果差很远。</p><h2 id="位置在最前面"><a href="#位置在最前面" class="headerlink" title="位置在最前面"></a>位置在最前面</h2><p>dewow 在整条处理流水线里是第一步，在带通、背景去除、增益之前。先把每道拉回 0 基线，后面才有干净的数据接着处理。顺带一个坑：dewow 减完会出负值，写回文件的时候有符号、无符号格式要分开 clamp，这个之前单独写过，不展开了。</p><p>dewow 说穿了就一件事：沿每道时间方向，减掉那条不贴 0 的慢漂，让信号回到围绕 0 的状态。它不解决反射弱、背景条带那些问题，但作为流水线的第一道清洗，基线不归零后面啥都谈不上。</p>]]>
    </content>
    <id>https://cyk.pink/posts/5b8c3e91.html</id>
    <link href="https://cyk.pink/posts/5b8c3e91.html"/>
    <published>2026-07-05T02:15:00.000Z</published>
    <summary>
      <![CDATA[<p>处理探地雷达（GPR）数据，拿来第一步基本都先做 dewow。刚接触的时候光看名字不知道它在去什么，后来弄明白了，记一下。</p>
<p>先说两个词。雷达每次发射接收，得到的是一条沿时间方向的采样序列，叫一道（A-scan）；把很多道沿测线方向排开，就成了二维的剖面图（B-scan）。dewow 处理的是「一道」内部的事。</p>
<h2 id="一道信号本来该围绕-0，实际常常整体漂"><a href="#一道信号本来该围绕-0，实际常常整体漂" class="headerlink" title="一道信号本来该围绕 0，实际常常整体漂"></a>一道信号本来该围绕 0，实际常常整体漂</h2><p>理论上，去掉直达波以后，一道雷达信号应该围绕 0 这条基线上下抖动——有反射的地方抖一下，没反射的地方贴着 0。实际拿到的数据不是这样，整条信号会慢慢往上拱一段、又往下沉一段，整体不贴着 0，像是在一个缓慢起伏的包络上叠加了高频反射。这个慢漂就叫 wow。</p>
<p>wow]]>
    </summary>
    <title>dewow 这一步去的是低频漂移</title>
    <updated>2026-07-05T02:15:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>CYK</name>
    </author>
    <category term="开发笔记" scheme="https://cyk.pink/categories/%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/"/>
    <category term="licence" scheme="https://cyk.pink/tags/licence/"/>
    <category term="keygen" scheme="https://cyk.pink/tags/keygen/"/>
    <category term="安全" scheme="https://cyk.pink/tags/%E5%AE%89%E5%85%A8/"/>
    <content>
      <![CDATA[<p>之前研究了一下 keygen（GitHub 上 keygen-sh&#x2F;keygen-api，一个开源的软件许可服务），想搞清楚工业级的软件授权到底是怎么组织的。看完最大的感受是，它早就不是那种「写个算法、算出一串序列号、客户端拿同样算法校验」的老路子了——那种算法只要被逆出来就全军覆没。keygen 把授权拆成了一套类似图模型的东西在服务端动态管。</p><p>整个体系围绕三个东西转：策略（Policy）、许可证（License）、设备（Machine）。策略是规则模板，比如「能激活 3 台机器、有效期一年、每 30 天必须联网一次」；许可证是卖给用户的实体，绑在某个策略上；设备是许可证真正消耗的额度，每激活一台就建一个 Machine 节点、扣一个名额。这套拆法把「卖了多少份」「每份能绑几台」「谁在用」分得很干净，加规则改策略就行，不用动许可证本身。</p><h2 id="离线验签靠非对称加密"><a href="#离线验签靠非对称加密" class="headerlink" title="离线验签靠非对称加密"></a>离线验签靠非对称加密</h2><p>我觉得这是它最核心的一手。光靠联网请求校验有个致命问题：破解者本地起一个假服务器（mock），把你客户端的请求全拦下来返回「合法」，校验就形同虚设。keygen 的做法是服务端用非对称加密（Ed25519）的私钥给许可证数据签名，客户端只内置公钥。这样它支持完全离线校验——只要签名验得过、数据没被改过，就认。私钥一直在服务端，客户端 mock 再像也没用，因为它签不出合法的签名。</p><p>这点其实跟自己做 JWT 授权是一个思路：私钥签、公钥验，把信任根钉在密钥上而不是网络上。</p><h2 id="在线激活是把设备和许可证绑死"><a href="#在线激活是把设备和许可证绑死" class="headerlink" title="在线激活是把设备和许可证绑死"></a>在线激活是把设备和许可证绑死</h2><p>用户拿到 key 之后在软件里填，客户端带着 key 和本机的硬件指纹去调 validate 接口。服务端查这个 key 合不合法、过没过期，没问题而且是新设备的话，就在后台给它建一个 Machine 节点、扣名额，然后返回一个短期有效的 token。指纹这东西是用来防「一码多绑」的——同一份 key 想在第十台机器上激活，名额不够就拒掉。</p><p>运行期间还有心跳：隔几天客户端回来打一次卡，策略里配了心跳的话，超时没报到，这台设备在服务端就被标记失效，下次得重新联网。这条主要是防退款之后继续用、以及及时回收已经不用的授权。</p><h2 id="断网环境走文件换码"><a href="#断网环境走文件换码" class="headerlink" title="断网环境走文件换码"></a>断网环境走文件换码</h2><p>真正麻烦的是机房、内网这种完全连不上外网的场景。keygen 给的是一套「文件换码」流程：断网机器上软件导出一个带本机指纹的请求文件；用户把它拷到能上网的机器、传到官网；服务端校验后用私钥签一个许可证凭证文件；用户再把它拷回断网机器导入，客户端拿内置公钥验签、比对指纹，过了就离线解锁。</p><p>绕了一圈，离线模式下信任还是落在「私钥签名 + 公钥验签 + 设备指纹」这三件套上，联网只是换个地方做这件事。</p><p>研究下来其实就一条主线：keygen 把密码学、设备指纹、并发名额、过期控制这些东西，解耦成了一套干净的 RESTful API。开发者不用自己在数据库里写一堆授权判断，客户端要么调它的接口、要么本地拿公钥验签，就能搭出一套还算像样的授权体系。它当然不是无敌的——私钥泄露、客户端被深入逆向都一样会破，但作为「许可即服务」的基础设施，这套抽象确实把脏活都收走了。</p>]]>
    </content>
    <id>https://cyk.pink/posts/6c1e8a3f.html</id>
    <link href="https://cyk.pink/posts/6c1e8a3f.html"/>
    <published>2026-07-04T01:12:00.000Z</published>
    <summary>
      <![CDATA[<p>之前研究了一下 keygen（GitHub 上 keygen-sh&#x2F;keygen-api，一个开源的软件许可服务），想搞清楚工业级的软件授权到底是怎么组织的。看完最大的感受是，它早就不是那种「写个算法、算出一串序列号、客户端拿同样算法校验」的老路子了——那种算法只要被逆出来就全军覆没。keygen 把授权拆成了一套类似图模型的东西在服务端动态管。</p>
<p>整个体系围绕三个东西转：策略（Policy）、许可证（License）、设备（Machine）。策略是规则模板，比如「能激活 3 台机器、有效期一年、每 30 天必须联网一次」；许可证是卖给用户的实体，绑在某个策略上；设备是许可证真正消耗的额度，每激活一台就建一个 Machine 节点、扣一个名额。这套拆法把「卖了多少份」「每份能绑几台」「谁在用」分得很干净，加规则改策略就行，不用动许可证本身。</p>
<h2 id="离线验签靠非对称加密"><a href="#离线验签靠非对称加密" class="headerlink"]]>
    </summary>
    <title>keygen 这套软件许可服务是怎么设计的</title>
    <updated>2026-07-04T01:12:00.000Z</updated>
  </entry>
</feed>
