起因

最近几年我有个习惯:常用的网页喜欢"装"成独立的桌面 app。一个窗口一个站,Dock 里独立图标,Cmd+Tab 能切,不会和浏览器里几十个 tab 混在一起。

但用着用着会发现一些挠头的小毛病:

  • 网页里有一个浮窗挡视线,每次都要手动关
  • 某个面板的字号偏小,眼睛累
  • 一段广告位想隐藏掉
  • 偶尔想改两行 CSS 让深色模式更耐看一点

这种东西用 DevTools 当场都能改,但代价是一刷新就没了。装 Tampermonkey / Violentmonkey 也行,但近一两年这类扩展为了配合浏览器的 MV3 安全策略,默认都要用户去开发者模式里手动勾"允许用户脚本"这种选项才能跑——自己用倒还行,想推荐给身边非技术的朋友用一下,光是讲清楚这几步就够费劲了。更别说有些站点的 JS bundle 我想直接换掉一小段——这种活儿用扩展本来也做不到。

我想要的其实很简单:

  1. 任何一个网页,输一行 URL 就能当独立 app 跑
  2. 我自己写的 userscript,能用 Tampermonkey 那套 @match 语法直接装上去——不走扩展、不用让用户去开发者模式里勾"允许用户脚本"
  3. 脚本就是磁盘上的几个 .js 文件,跨重启都还在,方便我自己 grep / git 管

试了一圈现成方案没有完全对得上的,所以索性自己写了一个,叫 Pouch。Rust + Tauri v2,macOS 和 Windows 都有打好的包。

核心思路

Pouch 在概念上其实就两件事:

第一件,把网页装进窗口里。 用 Tauri 起一个 webview,cookie、storage、origin 全都是站点真实的那一份,不做任何重写。你登录过的状态、保存过的偏好,跟你在 Safari/Edge 里看完全一样。

第二件,把你的 userscript 注入进去。 Pouch 内置了一个 Tampermonkey 风格的脚本调度器——扫 inject/ 目录,把符合 @match 规则的 .js 文件在 document_start 时机插进页面。整条链路不依赖任何浏览器扩展,没有"先去开发者模式里勾选项"的额外步骤,也不受 MV3 那套生命周期限制。

配置长什么样

一切都收敛在一个 TOML:

# 启动时打开哪些 URL,第一条是主窗口,剩下的各自独立窗口、共享 cookie
startup_urls = ["https://www.leelib.com"]

# 窗口初始尺寸:inherit/default/maximized/fullscreen 或自定义 width/height
window_dimensions = "inherit"

# 这些 URL 直接放行,不走 Pouch 的内部处理;通常用于 analytics / 字体 CDN / 三方静态资源
ignore_urls = [
  { suffix = "gstatic.com" },
  { suffix = "googletagmanager.com" },
  { suffix = "google-analytics.com" },
  { suffix = "cdn.jsdelivr.net" },
  { wildcard = "*.google.com" },
]

[updater]
auto_check = true

macOS 下这个文件在 ~/Library/Application Support/Pouch/hook.conf.toml,Windows 下在 %APPDATA%\Pouch\。改完按 Cmd+R(Windows 重启进程)就生效。

ignore_urls 提供四种匹配方式,按精度递增挑就行:host 后缀、host 通配、整 URL 通配、整 URL 正则。

注入脚本

inject/ 目录下放任意 .js 文件,文件头写一个 Tampermonkey 风格的元数据块:

// ==UserScript==
// @name   tweak example
// @match  https://www.example.com/*
// ==/UserScript==

(function () {
  console.log('[tweak] running on', location.host);
  // your tweaks here
})();

几条规则值得提一下:

  • 脚本运行在 document_start,比页面自己的 JS 还早,所以拦改 window.fetch、提前注入 hook 都来得及
  • 每个脚本各自包在独立的 try/catch 里,一个挂了不会拖累别的
  • @match 默认是 glob,*/;想用正则就写 @match regex:^https://...
  • 不写 @match 的脚本会在扫描阶段被跳过——这是故意的,省得你哪天不小心把全局脚本塞到所有站点里
  • iframe 里只跑显式命中的脚本,光写 @match * 是不会在 iframe 里运行的;想覆盖 iframe,单独写一条更精确的规则

SPA 的路由切换 不会 重新触发脚本(webview 没真正 navigate),如果你想 hook URL 变化,自己挂 history.pushState 就行。

一些可能不显眼但其实有用的细节

多窗口共享 storage。 配置里 startup_urls 写几条,Pouch 就开几个窗口。每个窗口是独立的 webview,但 cookie、localStorage 共享同一份——本质上等价于"同一个浏览器里的多个 tab,只是装在不同 OS 窗口里"。这对一个账号要同时盯多个面板的场景挺香。

标题栏的三个小按钮(macOS)。 右上角放了三颗:reveal data folder(直接跳到 inject/ 和配置文件那个位置)、reload from config(重新读 TOML 并重启)、toggle DevTools。原本是为了我自己调试方便,后来发现日常用最多的居然是第一个——改完文件想看磁盘上长啥样,按一下就完事。

安装走 Homebrew 和 Scoop。 macOS:

brew install --cask leaker/tap/pouch

Windows:

scoop bucket add leaker https://github.com/leaker/scoop-bucket
scoop install pouch

Windows 走 Scoop 是有意为之的——portable zip 不会被 SmartScreen 拦下来。没装包管理器的话,Releases 页.dmg.exe 直接下。

HTTPS 证书提示。 第一次启动 Pouch 会要一次系统级的信任提示——它需要本地 CA 才能看到、改写 HTTPS 流量。点一次同意,之后再启动都不会再问。

适合用 Pouch 吗?

我自己常拿它做几件事:

  1. 常驻型网页变 app。 工作里某个内部仪表盘、某个 webmail、某个 SaaS 控制台——Cmd+Tab 切起来比 tab 快得多
  2. 给老旧 web 应用打补丁。 厂商不更新了,但有个我每天用的 bug;写 5 行 userscript 永久治好
  3. 隐藏掉某个网页里让人心烦的元素。 一个 document.querySelector('.popup').remove() 写进 userscript,在 document_start 跑就完事,再也不用每次手动关
  4. 给某个站点做个人功能扩展。 自动展开折叠的评论、给页面加一组快捷键、自动登录跳转——一段 userscript 就够

不适合的场景也很明显:你想要 ad block、想要密码管理、想要扩展生态——那应该继续用 Chrome/Firefox + 扩展。Pouch 没那个野心,它只想做"webview + Tampermonkey 风格的脚本注入",把"我自己写的那点小补丁"持久地挂在我每天用的几个网页上。

最后

代码全开源,MIT,地址在 github.com/leaker/pouch

写这个东西的初衷其实挺朴素:MV3 之后的浏览器扩展给不了我想要的"零摩擦写脚本"体验,Electron 套壳又太重,DevTools 改完一刷新就没了,那就自己造一个。结果造着造着发现还挺好用——常用的几个网页都已经被我塞进去当独立 app 跑了,每个都带着我自己的几行小补丁,挺安心。

如果你也常觉得"这个网页要是能再改一点点就好了",可以试试。

起因

最近写了一个小工具:把一段 Markdown 渲染成 6 张小红书风格的 PNG 卡片,然后打包成 ZIP 让用户下载。写完跑了一下,撞见一个反常的现象:

  • 单张卡片导出大约 200ms,体感很流畅
  • 但点 Download All as ZIP 一次导出 6 张,整个过程要 4 ~ 7 秒

数学不对。6 × 200ms 应该是 1.2 秒左右,怎么也不应该是 4 秒以上。

把每一步的耗时打出来后,看到一个明显的冷热交替:

卡片序号toBlob 耗时
11300 ms
2212 ms
31205 ms
4187 ms
51189 ms
6210 ms

奇数张全部在 1.2 秒上下,偶数张稳定在 200ms 出头。结果就是 6 张总耗时 ~4.3 秒。

第一轮假设:字体内联导致 GC 抖动

ipecho.io 的卡片为了在导出的 SVG 里保留中文字体,需要把字体文件以 base64 形式塞进 @font-face,也就是 html-to-imagebuildFontEmbedCSS 那一套。我用的字体(思源黑体 + 一点 emoji 兜底)跑下来 fontEmbedCSS 大约 8MB。

我的第一反应:8MB CSS 嵌进每张卡的 SVG,每张卡分配 ~16MB 字符串,两张卡累计 ~32MB 正好够触发 V8 major GC。奇数张那次卡顿就是 GC stop-the-world,偶数张在新生代里拷贝就快了。

听起来很合理,于是开始按这个假设动手。

失败的尝试 1:共享 offscreen host

思路是:让所有卡片共享一个挂在 document.body 上的 offscreen host,把 8MB 的 <style>@font-face只挂一次,所有卡渲染时复用。

结果:4.3 秒 → 30 秒,恶化了将近 7 倍。

排查了一下才反应过来:8MB CSS 一旦挂到 document.body,就进了全局 document.styleSheetshtml-to-image 在 cloneNode + inline-styles 阶段会对每个克隆出来的节点遍历 styleSheets 做 CSSOM 匹配,等于每张卡都要把这 8MB 走一遍。等于把原本只在 SVG 里一次产生的成本,亲手挪到整个 DOM 上反复吃。

立刻回退。

失败的尝试 2:换 modern-screenshot

modern-screenshot 的 README 里写着 “2-4x faster than html-to-image”,那就换换看。

结果:4.3 秒 → 7.2 秒,反而更慢。

把每张卡的耗时再打出来,看到一个挺反直觉的现象:modern-screenshot 下根本没有冷热交替,6 张稳稳地全都是 1200ms。也就是说,原版 html-to-image 那个 200ms 的"快路径"其实是它内部某条隐性 cache 起的作用——modern-screenshot 重写的时候顺手把这条 cache 路径优化掉了,看起来更"现代",代价是失去了这条本来能复用的 warm path。

回退。

失败的尝试 3:用 blob URL 替换 base64 字体

再换个思路:8MB 的 data:font/woff2;base64,... 大头都在 base64 编码本身。换成 blob:http://localhost/xxx 这种 URL,引用只要几 KB,浏览器还能跨卡片缓存同一个 Blob。

写完跑了一下,pixel diff 直接 = 0,字体没渲染出来。

往下挖才看清楚:导出走的是 <img src="data:image/svg+xml,..."> 这条路径,浏览器把里面的 SVG 当成 opaque origin 上下文。而 blob: URL 是 same-origin scoped 的,跨上下文 fetch 直接被同源策略挡住。安全模型层面就走不通,没得绕。

一个反常观察推翻了假设

前面几招都走不通,索性反复多跑几轮,看能不能撞出点别的线索。还真撞到一件挺奇怪的事:同一个会话里再点一次 Download,6 张卡的总时间从 4.3 秒直接掉到 1 秒

要真是 GC 触发,每次 ZIP 都该重现冷热抖动才对。可这次抖动只在第一次跑的时候出现——第二次同样的代码、同样的字体、同样的数据,6 张全是 warm path:

卡片序号第二次 toBlob 耗时
1198 ms
2162 ms
3175 ms
4168 ms
5183 ms
6154 ms

总计 ~1 秒

浏览器内部有一层看不见的全局持久缓存。第一次 ZIP 那 4.3 秒里,大头其实是一次性的预热成本:

  • base64 解码 8MB woff2
  • woff2 二级解压
  • font tables 解析 + glyph cache 构建
  • V8 在 html-to-image 那条调用链上的 JIT hot path 形成

这套预热做完一次,整个 tab 生命周期里都还命中。回头再看冷热交替——其实就是奇数张刚好在为下一种新字体做预热,偶数张正好跑在已经预热好的路径上。根本不是 GC,是按需分摊的预热成本。

真正的解药:pre-warm

既然第一次跑就相当于在预热,那让用户永远遇不到这个第一次就好了。

办法很朴素:在用户实际点 Download 之前,先悄悄在后台跑一次极小的 dummy export,用同一份 fontEmbedCSS 把整条导出链路完整走一遍,把浏览器的缓存先填好。

实现要点:

  1. useVisibleTask$ 里挂 requestIdleCallback,避免和首屏渲染抢 main thread
  2. 创建一个 50×50 的 off-screen <div>,复用已有的 exportCardToPng 函数,pixelRatio: 1 跑一次
  3. 模块级维护一个 Set<fontFamilyId>,每个会话每个字体只 warm 一次
const warmedFonts = new Set<string>();

export async function prewarmFont(fontFamilyId: string) {
  if (warmedFonts.has(fontFamilyId)) return;
  warmedFonts.add(fontFamilyId);

  const idle = (cb: () => void) =>
    'requestIdleCallback' in window
      ? (window as any).requestIdleCallback(cb, { timeout: 2000 })
      : setTimeout(cb, 0);

  idle(async () => {
    const { exportCardToPng } = await import('./export-card');
    const dummy = document.createElement('div');
    dummy.style.cssText =
      'position:fixed;left:-9999px;top:-9999px;width:50px;height:50px;';
    dummy.textContent = '预热';
    document.body.appendChild(dummy);
    try {
      await exportCardToPng(dummy, { pixelRatio: 1, fontFamilyId });
    } catch {
      // 预热失败不影响业务
    } finally {
      dummy.remove();
    }
  });
}

这里有个 Qwik 特有的小坑:exportCardToPng 这个函数体很大,依赖了 html-to-image,要是直接在 useVisibleTask$ 顶层 import,QRL 会试图把它序列化进客户端 chunk,bundle 当场爆炸。改成在 task 内部用 await import() 做 dynamic import,就绕过去了。

实测结果

加上 pre-warm 之后第一次 Download All as ZIP 的耗时表:

卡片序号加预热后 toBlob 耗时
1215 ms
2178 ms
3192 ms
4165 ms
5203 ms
6171 ms

总计 ~1.1 秒,跟"用户点第二次"的体感几乎完全一致。

相当于把那 ~3 秒的预热成本,悄悄塞进了用户阅读页面的那几秒空闲里。等到他真去点 Download,已经站在 warm path 上了,体感永远只剩那 1 秒。

顺手写下来留个底,免得下次再被同样的 warm path 现象绕一遍。

将 PNG 图片转换为优化的 MacOS .icns 图标文件的 Shell 脚本,使用 sips + pngquant 两级压缩,自动调整尺寸至 512x512,支持透明背景。

功能

  • 自动调整图片尺寸为 512x512
  • sips + pngquant 两级压缩优化
  • 完整的依赖检查和错误处理
  • 显示压缩前后文件大小对比

依赖安装

# 仅需安装 pngquant
brew install pngquant

注:sips 和 iconutil 均为 MacOS 系统自带工具,无需额外安装

使用方法

下载脚本:generate-macos-icon.sh

# 添加执行权限
chmod +x generate-macos-icon.sh

# 基本用法(输出同名 .icns 文件)
./generate-macos-icon.sh my_icon.png

# 指定输出文件名
./generate-macos-icon.sh my_icon.png custom_name.icns

# 批量转换
for file in *.png; do
    ./generate-macos-icon.sh "$file"
done

核心目标

实现一个表格布局,当表格内容宽度超过容器宽度时,在表格容器内显示横向滚动条,而不是让内容撑开页面导致整个页面出现滚动条。

关键技术要点

1. Flex 布局的 min-width: 0 设置

这是实现的最关键部分。当使用 Flexbox 布局时,flex 子元素默认的 min-widthauto,这会阻止元素收缩到比其内容更小的宽度。

<!-- 错误示例:缺少 min-w-0 -->
<div class="flex">
  <div class="w-64 flex-shrink-0">侧边栏</div>
  <div class="flex-1">内容区</div> <!-- 内容会撑开容器 -->
</div>

<!-- 正确示例:添加 min-w-0 -->
<div class="flex">
  <div class="w-64 flex-shrink-0">侧边栏</div>
  <div class="flex-1 min-w-0">内容区</div> <!-- 允许收缩 -->
</div>

2. 容器的 overflow 设置层次

正确的 overflow 层次结构至关重要:

<!-- 表格容器结构 -->
<div class="flex-1 overflow-hidden"> <!-- 外层:防止内容撑开 -->
  <div class="flex-1 overflow-auto"> <!-- 内层:显示滚动条 -->
    <table class="w-full min-w-max"> <!-- 表格:保持内容宽度 -->
      <!-- 表格内容 -->
    </table>
  </div>
</div>

3. 表格的宽度设置

表格需要同时设置 width: 100%min-width: max-content

table {
  width: 100%;           /* 默认占满容器宽度 */
  min-width: max-content; /* 但不能小于内容的自然宽度 */
  table-layout: auto;     /* 自动计算列宽 */
}

4. Sticky 表头的实现

使表头在垂直滚动时保持固定:

<thead class="bg-gray-100 sticky top-0 z-10">
  <tr>
    <th class="whitespace-nowrap">列标题</th>
  </tr>
</thead>

5. 滚动条美化

使用 CSS 美化滚动条样式:

/* Firefox */
.overflow-auto {
  scrollbar-width: thin;
  scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
  scrollbar-gutter: auto; /* 预留滚动条空间,避免内容跳动 */
}

/* Webkit (Chrome, Safari) */
.overflow-auto::-webkit-scrollbar {
  width: 12px;
  height: 12px;
}

.overflow-auto::-webkit-scrollbar-thumb {
  background-color: rgba(156, 163, 175, 0.5);
  border-radius: 6px;
}

完整示例代码结构

<!-- 页面主容器 -->
<div class="h-screen flex overflow-hidden">
  <!-- 侧边栏 -->
  <div class="w-64 flex-shrink-0">
    <!-- 侧边栏内容 -->
  </div>

  <!-- 主内容区 -->
  <div class="flex-1 flex flex-col min-w-0"> <!-- 关键:min-w-0 -->
    <!-- 其他内容(如搜索条) -->
    <div class="flex-shrink-0">...</div>

    <!-- 表格容器 -->
    <div class="flex-1 overflow-hidden flex flex-col">
      <div class="flex-shrink-0">表格标题</div>

      <!-- 滚动容器 -->
      <div class="flex-1 overflow-auto">
        <table class="w-full min-w-max">
          <thead class="sticky top-0">...</thead>
          <tbody>...</tbody>
        </table>
      </div>
    </div>
  </div>
</div>

完整示例预览

线上预览

线上预览

关键要点总结

  1. min-w-0 是灵魂 - 必须在 flex 容器上设置,否则内容会撑开页面
  2. 双层 overflow 结构 - 外层 overflow-hidden 防止撑开,内层 overflow-auto 显示滚动条
  3. 表格设置 min-w-max - 保持表格内容的自然宽度
  4. 合理的 flex 布局 - 使用 flex-1flex-shrink-0 控制伸缩
  5. 使用 scrollbar-gutter: auto - 预留滚动条空间,避免内容跳动

常见问题

Q: 为什么滚动条出现在页面底部而不是表格容器?

A: 通常是因为没有设置 min-w-0,导致 flex 容器无法收缩。

Q: 为什么表格内容被压缩了?

A: 检查是否设置了 min-w-max (或 min-width: max-content)。

Q: 表格在窄屏设备上如何处理?

A: 可以考虑在移动端使用不同的展示方式,如卡片布局或允许横向滚动整个表格。

参考资源

在 Windows 调试一些程序的时候,有可能会遇到程序本身编译为始终以管理员身份运行的。而这些程序在拿到管理员身份的时候会启动一些保护以检测自己是否被调试注入等等。

而我们不希望这些程序以管理员身份启动。

如果是直接启动该程序,我们可以使用命令:

cmd /min /C "set __COMPAT_LAYER=RUNASINVOKER && start "" "(your app path)"

替换 (your app path) 部分为你们的程序路径

如果经常需要使用的话,还可以把该命令集成到 Windows 右键菜单里,通过注册表

RunWithoutPrivilegeElevation.reg

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\*\shell\Run Without Privilege Elevation]
@="Run Without Privilege Elevation"

[HKEY_CLASSES_ROOT\*\shell\Run Without Privilege Elevation\command]
@="cmd /min /C \"set __COMPAT_LAYER=RUNASINVOKER && start \"\" \"%1\"\""

点我下载:RunWithoutPrivilegeElevation.reg

但有时该程序是被另外的程序启动的,这时我们就无法执行命令来启动了。 不过不用担心,Windows 还给了我们另外的方案

为程序设置 Compatibility Flags

DisableAppRunAsAdministrator.reg

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers]
"(your app path)"="RunAsInvoker"

替换 (your app path) 部分为你们的程序路径

设置好后,无论是谁启动的该程序,程序都无法以最高的管理员身份运行了。

仗剑江湖WebMUD客户端

最近在学习AI与一些前端方面的知识,各种前端方向依然属于我不擅长的领域,也是在为后面一些可能的情况存储知识储备。

前段时间忆起只有老网虫才知道的在西陆版的WebMUD游戏 “仗剑江湖” 游玩的岁月。或许只是突然心生怀念,就把早之前2012年写的一个客户端程序(仗剑江湖MUD客户端)拿出来找个服进去逛逛。

意外的有个服里还有不少活人,结果聊的正开心时突然程序崩溃了。原本的程序对付老最古老的江湖版本应该是可以稳定跑的,目前来看可能是遇到了之前开发那东西时没遇到过的状况。后来实际中了解也知道了因为别人江湖添加了很多他自己独有的活动和內容。

现在想修复一下,可原来那时的程序源码早不知道丢哪里去了。

那就干脆重新撸一个,刚好试试目前不擅长的方向可以做到哪些事情。

使用说明

  1. 配置好 地址.txt 为你们游玩的江湖网址,例如我压缩包里内置了 追梦仗剑江湖

    如果需要游玩其他服,只用把这个文件里的地址修改为其他的江湖地址即可

  2. 没有账号的同学记得先去你们的江湖网页注册好账号,我这里就不加注册功能了。别人网站有可能有公告之类的提示信息,还是要去看看。

  3. 启动主程序 仗剑江湖WEBMUD客户端.exe 剩下的应该各位都是老手可以看得懂了

目录说明

  • 脚本 - 是存放脚本的目录,目前内置了另一个辅助工具里内置的所有脚本。
  • 脚本/按钮 - 是存放游戏图形区域下方 快捷按钮 的脚本,所有放在这个目录的脚本都会被加载到按钮里。
  • 脚本/挂机 - 是右下方 循环挂机 功能的脚本,只能进行重复间隔时间自动逐步执行的脚本功能。
  • 脚本/快速行走 - 是右侧 快速行走 功能的脚本,分为两级目录。第一级为分类目录,第二级为快速行走脚本名称,通常使用目标地点或目标人物名称作为文件名以方便搜索。 (记得善用 快速行走 下方的 搜索所有脚本 功能。另外可以在快速行走的脚本列表用 双击 来代替 快速行走 按钮进行执行)
  • 用户 - 是存放多角色信息的目录,会保存用户最后在线以及会话信息用于免登录的快速进入游戏功能。里面还存放着对应角色的系统日志,每次进入游戏后会自动加载历史日志。系统日志不用特意清理,本身会限制只最后5000条日志。
  • fonts - 是放字体的目录,里面有个我认为比较漂亮的字体会被加载到网页。如果你们觉得不漂亮,可以用同样 woff2 格式的字体文件替换成你们喜欢的
  • downloads - 是把游戏里的图片下载到本地缓存的目录,第一次进某个服会图片加载速度会比较慢。但缓存好了以后图片都从本地加载,可以帮巫师他们节省一些不必要的流量开销。如果某个服有更新图片资源,删除这个目录让程序自己重新缓存即可。
  • 窗口信息.json - 用于记录最后一次窗口大小的。每次启动后会按照上次的窗口大小还原,如果不小心窗口变的奇怪,就把这个删掉重新启动程序即可。
  • 其他那些奇怪的文件都是程序打包后运行时所需要的,如果不小心删除了某个文件程序可能会无法正常运行

下载地址

OneDrive:

仗剑江湖WebMUD客户端

本站资源:

仗剑江湖WebMUD客户端

后记

暂时就这些,更多自动化的功能暂时不会加。因为游戏服的大部分人包括我都更喜欢手动慢慢玩,就是体会这种慢节奏像读一遍互动式小说的乐趣。

如果发现问题,请联系邮箱回馈: admin@leelib.com

程序会在我修正完问题后自动发现更新并提示,只需要点击一下按钮刷新过后,不出意外的话使用的就是新版本了。

更新历史

1.0.1

  • 由于一些原因不再使用阿里云CDN,所以更新了一些功能用于适配新的CDN。
  • 更新 Electron 内核到当前最新 v30.0.8。由于目前新的 Electron 版本压缩包超过100MB,不再适合存储在 GitHub 里,所以额外建了资源站来存储当作备选下载线路

1.0.0

  • 完成基本功能

收到别人发来的压缩包,结果解压出来文件名是乱码。 这种情况在别人和自己使用不同的操作系统平台以及编码时经常发生,为了解决这一问题。自己编写了使用指定编码解压文件的程序。

zip_name_fixer.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import argparse
import zipfile


def main():
    parser = argparse.ArgumentParser(description='ZIP filenames fixer')
    parser.add_argument('path', type=str, help='path to the ZIP file.')
    parser.add_argument('encoding', type=str, help='encoding of the filenames inside the ZIP file.')
    parser.add_argument('-o', '--output', type=str, help='output directory for the extracted files.', default='.')
    parser.add_argument('-p', '--password', type=str, help='decompression password for the ZIP file.', default='')
    args = parser.parse_args()

    with zipfile.ZipFile(args.path, 'r', metadata_encoding=args.encoding) as zip:
        zip.extractall(path=args.output, pwd=args.password.encode('utf-8'))


if __name__ == '__main__':
    main()

点我下载:zip_name_fixer.py

使用方式: python zip_name_fixer.py [-h] [-o OUTPUT] [-p PASSWORD] path encoding

其中 encoding 就是python中常用的编码代码,例如: gbkcp437 等等…

同时顺手还写了一个针对 tar.gz 压缩包的。

tar_name_fixer.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import argparse
import tarfile


def main():
    parser = argparse.ArgumentParser(description='tar.gz filenames fixer')
    parser.add_argument('path', type=str, help='path to the tar.gz file.')
    parser.add_argument('encoding', type=str, help='encoding of the filenames inside the tar.gz file.')
    parser.add_argument('-o', '--output', type=str, help='output directory for the extracted files.', default='.')
    args = parser.parse_args()

    with tarfile.open(args.path, 'r:gz', encoding=args.encoding) as tar:
        tar.extractall(path=args.output)


if __name__ == '__main__':
    main()

点我下载:tar_name_fixer.py

使用方式: python tar_name_fixer.py [-h] [-o OUTPUT] path encoding

当遇到某人发来的文本內容内不是标准 utf-8 编码时,可以使用本程序来进行转换。

转换时遇到文本行尾使用了Windows专用的 CRLF 换行符时,也会将文本行尾统一更换为 LF。这样的好处是可以缩小文本文件的空间占用,并且可以保证该文件可以在基于Unix和Linux的操作系统以相同的格式显示。

encoding_line_ending_converter.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import argparse
import os


def main():
    parser = argparse.ArgumentParser(description='Text Encoding and Line Ending Converter')
    parser.add_argument('path', type=str, help='directory to be traversed and processed.')
    parser.add_argument('encoding', type=str, help='encoding of the text file.')
    parser.add_argument('-e', '--extensions', type=str, help='only process specified file extensions. use "|" as the separator, for example: ".txt|.log|.html"', default='')
    parser.add_argument('-q', '--quiet', action='store_true', help='do not output information during processing.')
    parser.add_argument("-v", "--verbose", action="store_true", help="increase output verbosity")
    args = parser.parse_args()
    extensions = tuple(args.extensions.split('|'))

    processed_count = 0
    for root, _, files in os.walk(args.path):
        for file_name in files:
            if len(extensions) == 0 or file_name.endswith(extensions):
                if trans(os.path.join(root, file_name), args.encoding, args.quiet, args.verbose):
                    processed_count += 1

    if not args.quiet:
        print(f'{processed_count} files have been processed.')


def trans(file_path, file_encoding, quiet, verbose) -> bool:
    """Convert file encoding and line endings."""
    # check file encoding
    encoding = 'utf-8'
    content = ''
    try:
        with open(file_path, 'r', encoding=encoding) as f:
            content = f.read()
    except UnicodeDecodeError:
        with open(file_path, 'rb') as f:
            try:
                content = f.read().decode(file_encoding)
                encoding = file_encoding
            except UnicodeDecodeError:
                if not quiet:
                    print(f'{file_path} the file encoding is not {file_encoding}.')
                return False

    # determine if a file uses LF line endings.
    if '\r' in content:
        # convert CRLF line endings to LF.
        content = content.replace('\r\n', '\n')

        # write content
        with open(file_path, 'w', encoding='utf-8', newline='\n') as f:
            f.write(content)

        if not quiet:
            print(f'{file_path} the file encoding and line endings have been converted to UTF-8 and LF.')
        return True
    if encoding != 'utf-8':
        # write content
        with open(file_path, 'w', encoding='utf-8', newline='\n') as f:
            f.write(content)

        if not quiet:
            print(f'{file_path} the file encoding has been converted to UTF-8.')
        return True
    if not quiet and verbose:
        print(f'{file_path} the file is now encoded in UTF-8 with LF line endings.')
    return False


if __name__ == '__main__':
    main()

点我下载:encoding_line_ending_converter.py

> python encoding_line_ending_converter.py -h

usage: encoding_line_ending_converter.py [-h] [-e EXTENSIONS] [-q] [-v] path encoding

Text Encoding and Line Ending Converter

positional arguments:
  path                  directory to be traversed and processed.
  encoding              encoding of the text file.

options:
  -h, --help            show this help message and exit
  -e EXTENSIONS, --extensions EXTENSIONS
                        only process specified file extensions. use "|" as the separator, for example: ".txt|.log|.html"
  -q, --quiet           do not output information during processing.
  -v, --verbose         increase output verbosity

其中 encoding 就是python中常用的编码代码,例如: gbkcp437 等等…

Header Content Footer 布局

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Tailwind CSS Box Layout</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style type="text/tailwindcss">
      html,
      body {
        font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
        color: #c9d1d9;
        background-color: #0d1117;
      }
    </style>
  </head>

  <body>
    <div id="box" class="flex flex-col h-screen justify-between">
      <header class="border-b p-5">Header</header>
      <main class="flex flex-1 flex-col justify-center items-center mb-auto h-auto bg-gray-900">Content</main>
      <footer class="flex justify-center items-center border-t border-gray-800 p-6">Footer</footer>
    </div>
  </body>
</html>

线上预览

预览