小名开开

在春天的光影里喘息

默认情况下,vmware 虚拟机的硬盘空间可以在 vmware 的虚拟机设置里调整大小,但只能『扩展』,却无法缩小。

截图
当输入较小数字时,按钮变灰,无法点击。

问题:为什么要缩小虚拟机硬盘

一,vmware 的虚拟硬盘文件 *.vmdk 默认是动态分配空间的,没有数据的虚拟 “硬盘空间” 并不占用真实主机的实际硬盘空间。当虚拟机内需要储存更多数据时,对应的 vmdk 文件会相应增大,以便容纳新增的数据。但当虚拟机内的数据被清理时,已经增大的 vmdk 文件却不会自动缩小。随着虚拟机的不断使用,『只增大不缩小』的情况会逐渐严重,直到 vmdk 体积达到预设的虚拟机硬盘完整容量为止。
临时文件、虚拟机快照、积累性的文件复制与删除操作等各种情况都会增大 vmdk 体积。vmware 针对这种情况提供了 shrink disk(压缩硬盘)功能,也就是在虚拟机数据已经被清除后,整体扫描一遍 vmdk 文件,清理无效数据,缩小真实体积。但这也有两个麻烦:

  1. 因为膨胀永远存在,所以压缩也要经常进行,难免感觉在做无用功。
  2. Linux 下 shrink 功能有 bug,需要配合 dd 命令先以 0 数据充,相当于先撑大再缩小,耗时数倍。参考:第四条豆知识

二,很多情况下,我们安装虚拟机的用途都比较单一,虚拟机的实际硬盘占用也往往小于 vmware 的推荐值。这是实际需求方面及可操作方面。

三,强迫症 / 好奇 / 无理由。

于是,根据实际使用情况,适当降低虚拟机的硬盘容量,将『使用膨胀』的上限卡住,释放真实硬盘空间并避免经常的压缩操作,对时间和金钱都有好处。尤其是现在 SSD 逐步标配的阶段,虚拟机硬盘容量设置不当造成的浪费是很可观的。

问题:如何操作

我以一个当初决策不当分配了 40GB 硬盘空间的虚拟机 Windows XP 为例,重新调整的目标为 10GB。一般来说,全新安装的 Windows XP 大约占 4-5GB 空间,则我们还有约 5GB 空间可用来安装测试软件等,足够了。如果不够就自行酌情决定调整目标。

一、清理删除当前虚拟机下的所有快照,如果这些快照很重要不能删,那接下来的操作对你就没意义了。

二、确认虚拟机硬盘是拆分成多个文件的动态分配硬盘。本例子中,该虚拟机的所有文件放在 D:\Program files\Windows XP.vmwarevm 目录下。用 VSCode 打开虚拟机目录下和虚拟机同名的 *.vmdk 文件,如 『Windows XP.vmdk』。找到 # Extent description 部分,检查描述部分,一看就懂了。如图:

打开 vmdk 时几种可能的情况:

  • 如果有多行描述,并且每行写了 SPARSE ,这就是我们需要的类型,拆分+动态分配磁盘。直接跳过后续转格式步骤,进入修改分区步骤即可。
  • 如果有多行描述,但每行写的是 FLAT ,则为拆分的预分配磁盘,需要转格式。
  • 只有一行描述,写的是 FLAT ,则是单一文件预分配磁盘,也需要转格式。
  • 如果乱码或者提示打不开,则是单一文件动态分配磁盘,还是需要转格式。

VMWare 在新建虚拟机时会默认选择第一种类型,但玩家当初可能出于性能等考虑,选择了其它类型的虚拟硬盘。那么就需要使用 vmware 自带的一个命令行工具

1
vmware-vdiskmanager.exe

转换成第一种类型。该工具在 vmware 安装目录下,默认为 “C:\Program Files (x86)\VMware\VMware Workstation\vmware-vdiskmanager.exe”。

转换方法:

该工具的命令行写法为:

工具名 参数1 源文件 参数2 参数3 目标名称
vmware-vdiskmanager -r(转格式) Windows XP.vmdk(例) -t(目的) 1(目的磁盘类型,1 为拆分+动态分配) newdisk.vmdk

该命令中, -r-t 1 两部分不需要变化,目标名称 随意,之后会改名覆盖源文件。源文件名 根据实际情况变化,而 工具名 通常要带上路径。

所以完整的命令大约是如下样子:

1
"c:\Program Files (x86)\VMware\VMware Workstation\vmware-vdiskmanager.exe" -r "d:\Program Files\Windows XP.vmwarevm\Windows XP.vmdk" -t 1 "d:\Program Files\Windows XP.vmwarevm\newdisk.vmdk"

回车以后开始执行转换:

转换完成以后,删除旧的 “windows XP.vmdk” 和多个关联文件 “windows XP-s0xx.vmdk”,然后把新的 newdisk.vmdk 重命名成 “windows XP.vmdk” 以替换。其它 newdisk-s00x.vmdk 不用变。

三,打开虚拟机,进入系统。

  • 在虚机机内,使用分区管理软件将硬盘分区缩小,并将空闲保留在右端,分区块保持在左端。我们的整体目标是从 40GB 缩到 10GB,则现在要多缩一点给后续操作留出余量。如图,暂时缩到 9GB。
  • Windows 7 以上系统,自带的磁盘管理就有『压缩卷』功能可以调整分区大小,WinXP 用的是 DiskGenius 免费版本。Win98 则需要寻找更古老的相应软件。
  • 中途可能需要重启虚拟机,无妨,调整完关闭虚拟机即可。

四,加减乘除:

再次打开 Windows XP.vmdk 文件,定位到 # Extent description 部分。这次重点放在第二部分数字内容上,经计算,该数字为每个分块文件对应虚拟硬盘的簇数量,每簇 512 Byte,2 簇 = 1KB。我们的目标容量为 10GB,也就是:

  • 10(GB)*1024(MB/GB)*1024(KB/MB)= 10485760 KB = 20971520 簇。

检查 # Extent description,前两个文件每个 8323072,则把第三行的数字改为

  • 20971520 – 8323072*2 = 4325376


并删掉后续各行及对应文件,我们就在 vmware 部分完成了虚拟机硬盘大小调整。

五,补足余量:

重开虚拟机。前次调整分区时,预留了 1GB 左右的余量。再次打开分区工具,把剩余未使用空间重新分配给各分区。

因为对计算机分区而言,由于存在分区表、启动扇区等原因,物理上的 10GB 硬盘空间在系统内表现是略小于 10GB 的,『内』『外』之间有差额,这对于虚拟机也一样成立。如果一开始就分区 10GB 而外部调整也等 10GB 的话,再次启动虚拟机就会报分区表错误,界时修改起来反而麻烦。现在这样按先预留余量,而后补足分区操作就没问题了。

六,清理死数据:

由于直接修改了描述文件,改小了簇数量,-s003.vmdk 里原本存于 4325376 之后的数据变成了永久的『死数据』。关闭虚拟机后,需要使用 vmware-vdistmanager -r -t 1 再作一次转换。虽说是转换,但目的却是清理死数据。清理完以后直接单独替换新旧 -s003.vmdk 即可。(想想为什么?)

————————————————-

相关细节:

  • 虚拟机为 Linux 系统时原理一样。利用 GParted 等分区工具,首先缩小使用分区,并调整未分区块到尾端。关机编辑 vmdk 描述,开虚拟机调整补分区足余量,再用 vmware 提供的工具清理死数据即可。
  • 预分配类型的虚拟硬盘必须先 vdiskmanager 转类型,不能直接操作。追求性能的话在编辑完再转回去。不过追求性能的话应该上 SSD,一力降十会。
  • 编辑 .vmx 和 .vmdk 文件不要用 windows 自带的记事本。
  • 虚拟机备份直接复制整个目录即可。
  • 扩容不需要按本文操作,使用 vmware 自带功能即可。

脚本文件里的 Hybrid Script(混合式脚本)

上文曾介绍了混合式脚本的原理和示例,这里补充一个实际适用的场景。

问题描述:

在 Windows 下,即使正确关联了 .ps1 的打开方式,Powershell 也无法正常运行脚本,只有一闪而过的报错信息。

正确设置了 PowerShell 的关联

使用录屏软件捕获到这一闪而过的错误信息

而首先打开 PowerShell 终端,并在终端里指定执行脚本,则可以正确执行:

脚本本身没有问题

错误原因:

这个错误原因其实挺显然的——在 Windows 下双击执行,等同于运行 PowerShell.exe,并向其传入了脚本文件的完整地址作为参数。但 Windows Explorer 在传递 %1 时没有加引号,PowerShell.exe 也没有对此做检查。结果就是,只要脚本所在的目录含有空格,PowerShell.exe 就会把空格前半段视为文件地址,而把空格后半段视为另一个运行参数。

而通过先启动终端再指定脚本运行正常,就是因为没有传参这个过程,Powershell 自行请求了脚本文件,就能正常执行。

解决办法

解决办法却不容易。修改双击打开时的参数设置为其加上引号,这个太困难了。修改 PowerShell 程序本身,为其添加参数检查更是不可能,只能坐等微软更新。

剩下的只有两种办法,一是永远把 .ps1 脚本放在没有空格的目录里。不但本目录不能有空格,上级目录一直到盘符所在的根目录都不能有空格。这其实也挺麻烦的。

另一种办法就是用混合式脚本:

  • 代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<# : test.ps1.bat
@ECHO OFF
powershell -noprofile "iex (${%~f0} | out-string)"
goto :EOF
:: 以上是 bat 代码,用以正确向 PowerShell 传递带空格路径 / 以下是 PowerShell 代码,示例用的 echo #>

echo ""
echo ================================
echo "You can run PS1 correctly here."
echo "But can't run it by double click."
echo ================================
echo ""

cmd /c pause | out-null

虽然我们要写的是 PowerShell 脚本,但为了让脚本能支持在含空格的路径下双击执行,我们不得不首先以批处理文件的形式调用 CMD,然后让 CMD 向 PowerShell 正确地传递带上引号的完整路径。这样,PowerShell 就能正确识别脚本文件的位置,执行混合脚本里的 PowerShell 代码段,完成最初预期的功能。

  1. 双击运行效果:
    混合脚本显示的是 CMD 的黑窗口

因为混合脚本首先运行的是 CMD,所以窗口变成了黑色。但 PowerShell 的脚本确实正确执行了。

题外话:

  1. 通常这种混合脚本我都会按语言命名为 ***.ps1.bat 以区别普通的 .bat,并提醒用户(也就是我自己)这需要 Windows 安装了 Powershell 才能正确执行。PowerShell 可以从 Windows 的系统组件里添加,Win10 则已经自带了。
  2. 为了截图方便,上文的示例代码最后一行其实又用到了 CMD 代码的 pause,所以这个示例代码严格来说是 CMD → Powershell → CMD 的三阶混合脚本。

因为日常需要,经常写一些脚本命令,比如 Windows CMD / PowerShell / bash 之类的。最近学习了一种新的脚本类型:Hybrid script,即混合式脚本,是在一个脚本文件内,同时使用多个语言的语法和对应功能。比如以下这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<# : hybrid.bat
@ECHO OFF
if "%~1" == "" goto SELECT
bin\youtube-dl -a "%~1"
goto :EOF

:SELECT
setlocal
for /f "delims=" %%I in ('powershell -noprofile "iex (${%~f0} | out-string)"') do (
bin\youtube-dl -a "%%~I"
)
goto :EOF
:: end Batch portion / begin PowerShell hybrid chimera #>

Add-Type -AssemblyName System.Windows.Forms
$f = new-object Windows.Forms.OpenFileDialog
$f.InitialDirectory = pwd
$f.Filter = "Text Files (*.txt)|*.txt|All Files (*.*)|*.*"
$f.ShowHelp = $true
$f.Multiselect = $true
[void]$f.ShowDialog()
if ($f.Multiselect) { $f.FileNames } else { $f.FileName }

这段代码的功能是调用 youtube-dl.exe,按预先准备的待下载视频列表指示,下载所有视频。有趣的是,如果你通过拖拽的方式,把列表放到脚本文件图标上,则脚本直接开始按列表下载。

如果没有拖拽,而是双击打开脚本文件,则会跳出一个选择文件的 Windows 对话框,让用户选择一个或者多个列表,然后再开始下载。

这是一个用户体验相对比较好的方法。虽然这脚本只是自用,但我自己也是比较烦命令行的。至于任务是下载视频,或者处理音频,亦或删除目录什么的,只是中段执行不同,和本文主旨无关。

这脚本的核心难点,便是『如果没有拖拽列表文件,则打开窗口让用户选』这个需求点。CMD 是没有 Windows 图形界面下的对话框功能的,但 PowerShell 有。因此便有了这个 Hybrid Script 脚本。

脚本前半段是 Batch 代码,输入下载列表文件路径,执行下载。这个『下载列表文件路径』,或者是用户拖拽获得,或者是用户在选择窗口中操作获得。后半段则是 .ps1 代码,用来绘制选择窗口并把列表路径返回 CMD。

但问题在于,两种代码并不兼容。CMD 并不能识别 PowerShell 代码,而 PS 也无法识别 Batch 代码。如果普通的执行对方的代码,一定会报错的。

于是我们看到,上面这段代码里似乎有几行奇怪的代码:

1
2
3
4
5
<# : hybrid.bat
......
goto :EOF
......
:: end Batch portion / begin PowerShell hybrid chimera #>

没错,就是这几行特殊代码,以及另一个非常重要的变量 %~f0,决定了 Hybrid Script 的可行性。

事实上,<# ... #> 是 PowerShell 的注释代码,PS1 执行器遇到它时,会直接忽略两者中间的所有内容,执行后面的代码。而 <# :hybrid.bat 对 CMD 而言是个没有指定目标的左向重定向命令。因此 CMD 确实执行了第一行,但没有任何效果。

对于 PS 的结束注释符号 #>,在 CMD 看来确实有意义。但我们提前用 goto :EOF 直接跳到脚本最末(End-Of-File)的办法,把这一行以及后面的所有代码都跳过了。于是这些代码在 CMD 下的对错就无关紧要了。

也就是说,在 CMD 眼睛里,这段代码其实是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
<# : hybrid.bat
@ECHO OFF
if "%~1" == "" goto SELECT
bin\youtube-dl -a "%~1"
goto :EOF

:SELECT
setlocal
for /f "delims=" %%I in ('powershell -noprofile "iex (${%~f0} | out-string)"') do (
bin\youtube-dl -a "%%~I"
)
goto :EOF

而在 PowerShell 眼里,这段代码是这样的:

1
2
3
4
5
6
7
8
Add-Type -AssemblyName System.Windows.Forms
$f = new-object Windows.Forms.OpenFileDialog
$f.InitialDirectory = pwd
$f.Filter = "Text Files (*.txt)|*.txt|All Files (*.*)|*.*"
$f.ShowHelp = $true
$f.Multiselect = $true
[void]$f.ShowDialog()
if ($f.Multiselect) { $f.FileNames } else { $f.FileName }

以上都明确以后,中间那句

1
powershell -noprofile "iex (${%~f0} | out-string)"

的作用也就容易理解了:Batch 脚本调用了 PowerShell 的执行器,并把这个 hybrid.bat 自己(在 Batch 代码中,%~f0 这个变量就是指脚本文件自己)传递给了 PowerShell。

PowerShell 接到消息以后,又执行了一遍这个 Hybrid.bat。这次是 PS 视角,因此上来就忽略了前面整大段的“注释”,直接从 Add-Type -AssemblyName System.Windows.Forms 这段开始,绘制窗口,等待用户选择,获得列表文件路径,然后再返回给 CMD。CMD 最后再执行 for /f "delims=" %%I in ( ...... ) do ( ...... ) 的部分,并根据 goto :EOF 的指示,跳过剩余代码,避免了在 CMD 环境下的报错。

因此,为了正确执行功能,Hybrid Script 的核心思想是:

1. 利用两种语言的注释符号的不同,隐藏非执行环境下的代码。
2. 灵活应用两种语言的特性,确保任一语言下的注释符号本身,对于另一种语言没有负面效果。
3. 首先执行的语言 A,需要把脚本文件自身的路径,传递给另一种语言 B 的执行器。
4. B 语言的执行器,忽略掉被 B 语言注释符号包裹起来的 A 语言代码,执行 B 的代码,如果需要的话,把执行结果返回给 A。
5. A 继续执行剩下的部分,忽略掉被 A 语言的注释符号包裹起来的 B 语言代码。
6. Hybrid Script 至少会执行两遍,可能更多。

———————————————

充分理解 Hybrid Script 思想以后,我们就知道这并不仅仅限于 Batch 和 PowerShell 脚本的混合了。常用的几种脚本语言都可以实现混合代码。实践中:

1. Windows Batch & Windows PowerShell
2. Windows Batch & Linux Bash
3. Windows Batch & Python
4. Windows Batch & Javascript
5. Linux Bash & Python
6. Linux Bash & Javascript
7. NodeJS & Python
8. …

等等组合都可以写出相应的混合代码脚本。

Hybrid Script 能同时利用两种甚至多种语言的方便特性。并且对于各语言组合,相互注释的『套路』是固定的,几部分代码实际功能如何变化并不影响套路。但其实整体来说适用性不广,一来机器上需要同时有两种语言的运行环境,二来 Python JS C# 等几种主流语言都有完备的功能集和函数库,不需要跨语言写作。三来,即使真的需要两种语言,大部分情况下也可以写成两个脚本互相调 用执行。

只有很少的几种情况下需要考虑 Hybrid Script,包括:

1. 因为传播渠道问题,不适合拆分成多个脚本的。比如互联网上常见的『复制粘贴代码到记事本改后缀为 .bat 然后双击运行』。
2. 代码票友,对 A 语言和 B 语言都不甚精通,或者从 A 转 B 的学习过程中,B 还不甚了解,需要用 A 语言的代码补足。
3. A 语言确实功能有限,但胜在编码方便历史普及率高。而产品虽需要却也只需要 B 的极小功能。比如本文示例。
4. 写着玩。


5. 解决 PowerShell 无法正确处理文件路径带空格的 BUG,参见:

Hybrid Script 附一则:解决 PowerShell 无法双击打开路径含空格文件的 Bug

今天遇到一个奇怪的情况,在使用形如:

=Match(A2,A:A,0)

的 Excel 公式查找时,居然返回了 #N/A 的结果。逻辑上来说,在自己所在的数组里查找自己,不应该得到错误结果,事实上这个公式的值只可能是 1 或者 2 才对。

经过检查,发现问题出在字符 ~ (波浪号)上,即键盘上 ESC 键的下方,数字 1 的左边那个键的 Shift 上档符号。

Excel 官方帮助文档 中,找到了相关的解释。

如果 match_type 为 0 且 lookup_value 为文本字符串,您可在 lookup_value 参数中使用通配符 – 问号 (?) 和星号 (*) 。问号匹配任意单个字符;星号匹配任意一串字符。如果要查找实际的问号或星号,请在字符前键入波形符 (~)。

这里提到了,当使用 Match() 函数进行精确的文本查找时可以使用通配符 ? 和 *,当用户确实需要查找问号/星号时,则可以用 ~? 和 ~* 来表示。这里没有提到的是,如果用户需要查找 ~,其实也需要通过两个波浪号 ~~ 来表示。转义符自身也需要转义表达,也算是一般规则了。

所以,如果一个单元格中包含了波浪号 ~,则当这个单元格作为被查找的数据的一部分时,是正常的。但当它同时作为 lookup_value 时,则会在转义后变得无实际意义。于是导致了 Match() 函数查找自身所在数组时产生的错误结果。

遇到这个问题时,我正在处理由家用下载机长年积累下的大量动画片资源。用 Bash 获得所有的动画目录名、文件名,并试图尽量删减一些重复资源。文件名系统中并不存在『 ? * : < > | / \ ” 』等符号,但偏偏允许波浪号 ~ 的存在。又因为在 Excel 的默认字体中,波浪号并不显示为扭曲的波浪形状,而是略长的横线,如图,于如是尴尬便发生了。

解决办法:

给 lookup_value 添加一个 SUBSTITUTE() 函数进行修正,即:

1
=Match(Substitute(A2,"~","~~"),A:A,0)

即可得到期望结果。

一句话总结:

match_type 为 0 且 lookup_value 为文本字符串时,使用 =Match() 函数时需要注意 lookup_value 是否包含 ~ 、?、* 并根据需要预先做处理。

Ikaruga「斑鸠」 on Steam

来,跟我念:

黑机吃黑弹躲白弹打白鸡留黑鸡给白队友遇白光切白机。
白机吃白弹躲黑弹打黑鸡留白鸡给黑队友遇黑光切黑机。

黑白黑白黑白黑黑白白黑黑黑黑白黑白黑白黑白黑白黑黑黑白黑,
变变变变变变变定变定变定定定变变变变变变变变变变定定变变。

黑白黑黑白白白黑白白黑白黑白白黑黑白黑白黑黑黑白白黑黑黑黑黑白白白黑黑黑白黑黑白白黑黑白黑白黑黑黑黑白白白白白白,
变躲吸吸变吸吸躲吸吸躲吸变躲躲吸吸躲吸躲吸吸吸放大招吸吸吸吸变吸吸变吸吸变躲躲吸吸变吸躲吸躲吸吸吸躲变吸吸放大招。

……差不多这个感觉。

Ikaruga 斑鸠

如果说彩京、虫姬、东方是用层层叠叠的华丽弹幕压迫你的灵魂让你手脚冰凉让你奋起反抗让你一世积累尽殁此役,那么斑鸠则是用至简与朴素唤醒你心底的温暖,唤起你对理想与光明世界的向往。

这个世界没有遮天蔽日的弹幕,每一个子弹都可以直接吸收无需躲避,
这个世界没有血条畸长的敌人,即使是关底 Boss 也挡不住几轮蓄能爆发,
这个世界的敌人甚至还会在死去时抛出剩余的能量帮你充能,
这个世界不断地告诉你什么叫理想、试练、信念、现实与轮回。
这个世界总是将光明放在你的眼前,触手可及。

一个完美的世界。

这个世界里里唯一不完美的是你。是屏幕前双手颤抖面目狰狞的你,是竭力紧抓手柄胡乱变色仓皇逃窜的你。但这个世界依然爱着你,从内到外都爱着你。

没有 给你任何 随机出现的敌人,只有约定之时,只有约定之地。
她给了你远超需要的强大技能,蓄能爆发,让你在瞬间打倒最强大的敌人。
她给了你 异色双倍伤害,给了你 三同色击破链式加分,给了你破敌 残弹 额外蓄能。
她还给了你 竖屏模式,将显示器旋为竖置,世界便扩大八分。
她甚至给了你 单手柄双机模式,让最孤独的你也能与自己一起游戏。
她唯独只是 没有 给你 网络联机 模式,她是完美的,她不需要的,便是不需要的。

你只需要做好一件事,在该变白时变白,该变黑时变黑,你就能回应她的爱,双双走向高潮。

你怎么就做不到呢。

甚至最后的最后,她为了如此不成器的你,把 无限命模式 也给了你。

完美的她,深深地爱着毫不完美的你,爱着什么都做不到的你,爱着靠无限命死乞白赖撒泼打混的你,爱到了最后。

这份爱,只需 36 人民币,打折 18。

参考老文章: Overwatch Toolchain 解包方式考察\,OW 音频小站一直以来使用原始文件 hash 的办法,便每次更新以后,都能区分旧数据和新增数据,使得每次更新的条目数量都在合理范围。但从版本 1.14 开始,暴雪更新了内部数据格式,这个是由蓝帖明确提到的,目的是减少之后的每次客户端更新时的下载。

但这对本小站来说不亚于一次冲击。这意味着,直到 1.13 版为止的条目,和从 1.17 版开始的所有条目,无法从数据角度建立『新』与『旧』的联系。

也就是说,明明从用户角度来说是同一句台词,音频波形也一致,但由于 OW 内部格式的变化,导致 hash 计算的结果变化,使得从数据角度来说,它们变成了两个不同的条目。

比如: 【D.Va】D.Va 一分,坏蛋零分。(1.17 版本) 的 hash 计算结果是 5908654fbc3965232689836abb249c57,而
【D.Va】D.Va 一分,坏蛋零分。(1.16 或更早版本) 的 hash 计算结果是 1d99c1739f5b0844f57f3a1a5fbb4580

我做了很多尝试,试图恢复一些信息,把这些本质相同的音频重新联系起来,有些成功了,有些失败了。最后我把匹配成功部分更新到原有条目上,匹配失败的部分按『新增音频』对待,添加到数据库里。

  1. 尝试使用音频分析软件批量比较,如果成功,理论上这是最好的办法。
    • 这是本质的解决办法,如果两个音频文件的波形完全一致,则两个音频当然是一样的。
    • 尝试了 Audacity 和 Similarity,前者缺乏批量功能,后者无法对这种只有一两秒的音频进行比较,均告失败。
  2. 尝试使用新版本拆解工具拆解旧客户端,失败,但得到了启发。
  3. 因为拆解所用的 Toolchain 工具链(简称 TC)本身有自己的文件名系统,而我亦保留了过去所有版本的原始拆解数据,理论上能通过新旧版本文件的相同路径名等信息恢复联系。
  4. 测试以上两条 D.VA 音频的 TC 路径分别为:
    • 旧(1.13):cn\heroes\D.Va\Sound Dump Full\_Base\000300000057\000000020231.wem
    • 新(1.18):cn\HeroVoice\D.Va\00000000059F.078\000000020231.wem
  5. 测试成功。
  6. 进入实操,发现由于旧版本拆解工具的不完全,数据存在错误,依然有许多坑要填:
    • 一,多个不同的 TC 文件名对应同一个 hash,即多个 1.18 新文件条目对应同一个 1.13 旧条目 hash,错误在旧版拆解工具自带的文件名系统有 bug。
      • 解决办法是视为多个条目,并将旧条目的听写内容更新到各个新条目上。
      • 对于网站用户来说,同一个关键词可能会搜索出几条内容,但其中应该至少有一条是对得上的
    • 二,同一个 TC 文件名对应多个一个 hash,即同一个 1.18 新文件条目对应多条旧的 1.13 旧条目,显然错误还是旧版折解工具的不完善
      • 解决办法只能是人工筛选按多条里面正确的那条。
      • 大约一共有 1200 条这种类型的错误,一条一条听完。网站用户肯定会得到正确的结果,因为这部分我都筛完了。

这次更新正常应当在 1.14 新版数据格式更新,1.17 新版拆解软件开发成熟以后更新,但这数据处理是真 TM 麻烦啊,烦死了。

Grep 字典式匹配输出

1
grep -f source.txt dictionary.txt > output.txt

SED 若干则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 匹配成功输出
sed -n '/匹配成功/p' file.txt >output.txt

# 多个脚本并行执行
sh script01.sh & sh script02.sh & sh script03.sh

# 一般的正则替换
sed -i -e 's/^[0-9a-f]\{2\}/target\//g' a.txt

# 文件名无空格的列表输出
for file in $(find lib -name '*.txt'); do
echo "$file" >>liblist.txt
done

# 文件名有空格的列表输出
find lib -type f -name '*.txt' -print0 |
while IFS= read -r -d '' file; do
echo -e \"$file\" >>liblist.txt
done

# 计算文件 hash 值,用 cut 切掉输出时自带的文件名
hash=$(md5sum "$file"|cut -c1-32)

CMD 快速删除文件

1
2
3
4
5
6
7
:: 极快速删除所有文件
del /f/s/q *.*

:: 快速删除目录与子目录与文件
rmdir /s/q dir

:: 后者包含前者,但前者速度比后者快

Excel:从右往左查找某个字符。

1
=FIND("|",SUBSTITUTE(A2,"/","|",LEN(A2)-LEN(SUBSTITUTE(A2,"/",""))))

原理,使用 Substitute() 将所查找的字符替换为空,则长度差代表该字符出现的次数。将该次数作为 substitute 函数的第四参数,用一个特殊字符再度进行 Substitute() 替换,然后 Find() 该特殊字符。注意替换用的特殊字符(这里是"|",即Shift+\)不能在A2中出现,否则必须换一个。

目前适用于 ngacn 论坛帖子,效果图如下:

启动脚本后,会依次询问你 nga 的帖子 id,总页码数量和帖子名称。脚本会自动检查是否之前已经抓取过该帖子并在上次抓取的最后一页开始继续往下抓取。

每个帖子的抓取结果会放在独立的目录,并以”帖子id”-“自定义名称”的形式储存

注意事项:

  1. 脚本会优先检查是否有 $tid-xxx 形式的目录,只要 $tid 一致,脚本就认为本次会在旧的内容基础上往后抓取截图,xxx 的名称可以是任意的,不检查。假如已经存在一个 『12345-上山打老虎』 的目录,而你第二次输入的是 『12345-钻石王老五』,则实际更新的依然是『12345-上山打老虎』 目录。
  2. 目录名称中间的减号必须保留,否则脚本会忽略已存在的目录,按新目录工作执行。目录内的 1.update.sh 文件也必须保留。
  3. 我的运行环境是 Win10 64 位 1703 版本,不保证在其它机器上能正常运行。在 Win10 下脚本文件可以直接双击打开,在Win7 下可能需要右键点击并选择用 Git Bash 打开。
  4. 需要预先安装该脚本所需要的环境才能正常运行,包括 node、bash 等。

安装与准备工作:

  1. 下载并安装 Node for win 使 Windows 获得本截图脚本所需的 node 语言支持: 下载网址: https://nodejs.org/en/download/ ,选择下载 LTS 版本下对应的 32位 或 64 位 Windows Installer。安装一路 next 就行。
  2. 下载并安装 Git for win 使 Windows 也能使用 Bash 脚本。下载网址:https://git-for-windows.github.io/ ,也是一路 next 默认安装就行。
  3. 从开始菜单找到 Git Bash,,点击运行,在黑色窗口内依次输入以下内容:
1
2
3
npm config set registry https://registry.npm.taobao.org
npm i -g npm@latest
npm i -g pageres-cli

效果:

  1. 新建一个目录,起个名,比如叫『nga截图收藏夹』之类的。
  2. 在目录下新建一个文本文件,复制粘贴以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/bin/bash

#User input

tid=""
totalpage=""
title=""
while [[ ! $tid =~ ^[0-9]+$ ]]; do
echo -e "输入 NGA 帖子 id:"
read -r tid
done
while [[ ! $totalpage =~ ^[0-9]+$ ]]; do
echo -e "指定该帖子的总页数:"
read -r totalpage
done
echo -e "起个标题备注(可以自定义):"
read -r title

#Check if $tid follder is exist

for _dir in "${tid}-"*; do
[ -d "${_dir}" ] && targetdir="${_dir}" && break
done
if [ -z "$targetdir" ]; then
echo "建立一个新目录"
foldername=$tid"-"$title
mkdir "$foldername"
cd "$foldername"
touch 1.update.sh
echo -e "#!/bin/bash" >>1.update.sh
echo -e "startpage=\"1\"" >>1.update.sh
echo -e "totalpage=\""$totalpage"\"" >>1.update.sh
echo -e "tid=\""$tid"\"" >>1.update.sh
echo -e "rm \"page\"\$startpage\".png\"" >>1.update.sh
echo -e "for i in \`seq \$startpage \$totalpage\`" >>1.update.sh
echo -e "do" >>1.update.sh
echo -e " page=\"http://bbs.ngacn.cc/read.php?tid=\"\$tid\"\&page=\"\$i" >>1.update.sh
echo -e " fn=\"page\"\$i" >>1.update.sh
echo -e " echo \$page" >>1.update.sh
echo -e " pageres \$page --filename=\$fn" >>1.update.sh
echo -e "done" >>1.update.sh
echo -e "sed s/^startpage=\\\".*\\\"\$/startpage=\\\"\$totalpage\\\"/g 1.update.sh >u.tmp" >>1.update.sh
echo -e "mv -f u.tmp 1.update.sh" >>1.update.sh
else
foldername=$targetdir
echo "发现旧的工作目录,在此目录上更新:"
echo $foldername
cd "$foldername"
sed s/^totalpage=\".*\"$/totalpage=\"$totalpage\"/g 1.update.sh >u.tmp
mv -f u.tmp 1.update.sh
fi

sh ./1.update.sh

echo "Work Done"

随便起个名,并把后缀名改为 .sh。如何修改后缀名请自行百度。

使用:

  1. 双击,依次输入帖子 id,最后一页页码和标题就可以用了。

尾注:

  1. 如果你懂得简单的编程,可以尝试阅读脚本,并自行修改以抓取其它网站的贴子。这个脚本的原理就是利用 nga 帖子很有规律的地址,通过指定帖子 tid、页码 pageid 拼接出需要抓取的指定页面网址,然后反复调用 pageres 去抓取。实际进行抓取的是一个叫 pageres 的 npm 小程序。
  2. 如果你懂得简单的 JS,你可以自行指定 pageres 的抓取参数,甚至包括登录的 cookie 等,以实现『个性化抓取』。
  3. 如果你会完整的 node 开发,希望这个脚本也能为你提供一些参考。

小站因为忙停更了一段时间,结果被各种催更。值此国庆佳节加之周末免费,趁势也更新一下,再开些空头支票。

  • Done:更新末日铁拳、地平线月球基地及其它英雄新增语音。(1.13 版客户端拆解)
    时间有限,只处理了增量更新部分。旧数据的疏漏错误更新后依然存在。他日有空再逐步更新旧数据。

  • Done:去掉了早期开发的赞与踩功能。
    在去年小站刚出来时,曾指望让热心用户通过 vote / devote 功能,把『有内容的音频』也就是台词、音乐等条目上浮到前几页,让意义不大的嘈杂环境音下沉。但事实看来结果不好,一来是用户并没有很好地理解该功能。后台数据显示,被踩得最多的是源氏的龙神拔刀,其次是麦爹的午时已到。想必是冤魂太多,多少发泄一下吧╮( ̄▽ ̄”)╭。二来呢,我看到这个数据以后也就懒病发作,没有继续考虑改进该功能了。后来还短暂地改成『倒放音频』,也就是猎奇一番,甚不实用。今日该功能彻底取消,就当是当年那几千投票,终于修正成果了吧。

  • Done:调整了下界面,加了圆角、阴影,js 代码的报错部分也处理了。
    主要新增数据会用黄色标签显示在上方。条目按钮加了细微的圆角和渐变阴影,整体会更柔和一些。同时每页条目数从 200 条减少到 100 条,相应的显然页码数量也加了一倍。因为网页背景本身是守望的全屏截图,故整体风格不会有大的变化,没有前后对比,不仔细看的话应该看不出来。修正 JS 报错这事其实也可做可不做,只要不搞死页面,能正常执行功能,jQuery 也就这样了。

  • Done:加了打赏按钮。
    这个没啥说的,目前平均也就每周一罐可乐,但听见叮咚声时的好心情很值钱。摊开算收益的话应该现在就把小站关掉,每年能省千儿块服务器钱。

  • Done:更新 36729 条音频的 MP3,音质略有提升。
    音频文件的处理路径是游戏客户端→.wem格式→.ogg格式→.mp3格式,理论上每一步都会有损耗。为了能在网页上直接播放必须使用 MP3,但之前在把 OGG 转为 MP3 时我选的是 LameXP 里的质量等级 2(一共 10 级),现在我想通了,把质量等级改成了最高的 0。当然,质量越高,mp3 的体积也越大,从质量 2 到质量 0 体积平均会增大 50%,也就是大概每个文件 20KB 会增加到 30KB。

    这对视频制作者会有所帮助,下载的音频在嵌入视频后的音效会有略微的提升。对 Lame 编码而言,质量 2 到质量 0,新增的数据主要集中在轻微的、高频的或者低频音域。这些音域的补足会让音频的回声、立体感和通透性更好一点点点点,就当是信则有吧。 缺点是,对于使用手机流量访问的用户流量的消耗会增加。目前统计的平均每条数据大概是 28KB,请量力而行。至于对网页响应速度则是几乎没影响的,你的主要延迟是来自于与 Github.io 这个网站(我拿来存音频源)之间的通讯延迟,至于下载的数据是 20KB 还是 30KB,差的不过是 0.0x 秒而已。

  • Done:当条目文字很长时改用换行式,取代当前的隐藏式。
    这个功能和上一条类似,不难,但是会比较烦。换行以后会涉及到每行高度的变化,于是打开编辑面板时的对齐代码就需要调整。同时变化的高度也意味着右侧的齿轮按钮也要相应变化,现在它的高度是写死的。更麻烦的是 Input 框的高度自适应我没做过,可能要换用 contenteditable 的 div,要调整不少代码。好处当然也是显而易见的,长句子台词可以一次看完整了。

    Input 框暂时先不修改了,太麻烦要查资料,一时搞不定。条目本身现在是换行式的了。

  • Done:编辑条目时按 Ctrl+Enter 直接提交当前条目并自动打开下一条的编辑界面并同时播放音频。
    我自己,目前还是小站音频条目听写的第一贡献者,我觉得我在编辑条目时遇到的麻烦,你们肯定也遇到了。这个 Ctrl+Enter 的连续编辑提交功能应该会对热心的贡献者比较有帮助。当然伸手党是永远不会遇到这种麻烦的,但谁管你们那么多呢。

    我还没写过根据键盘组合键执行功能的代码,不过想必 Google 一下很容易就能得到答案。这功能不难,但是会比较烦。

  • Todo:编辑界面的标签钩选稍微改进一点。
    打算把现在的钩选换成和顶部筛选框类似的变色选中,顺便加上和英雄主题色相同的背景色。估计也挺麻烦的,tag 数据都是根据数据库生成的,现在数据库里没有英雄颜色,于是得改后端代码了。

  • Todo:逐步修正音频标签数据。
    这个没啥说的,其实主要依靠的是拆解软件的更新。之所以写在这里,是因为现有拆解结果,在数据处理上还能有少许改进,只是要花不少时间。

你看,写完这个,又快天亮了。

第一个故事,姑且称为杨大哥和徐小妹的故事吧。

前段时间给父亲电话,他颇为无奈地告诉了我这事。我又给杨大哥打了电话,聊了很多,也感触颇多,真是觉得人生如戏,戏如人生。

杨大哥不是什么黑道大哥,在我父亲眼里,他一直是个普通学生。父亲任教三十余年,也算是桃李满天下,可毕业多年还能来探望,还能多少愿意说几句的,也就那么几个,杨大哥是其中一个。

这是个很老套的开头,无非是那时杨大哥还是杨小哥,敷衍学业,荒废才智,在高一分班后遇上了他生命中的第一段感情,括号单相思括号完。杨小哥和所有单纯少年一样,把表白当成宣战,把体贴弄成骚扰,搞得徐小妹很是困扰了一段时间。她家长愤怒地跑到校长那里堵截,弄得杨小哥差点被开除。事情最后平息了下来,当时父亲作为他的班主任,很是顶了不少压力。

杨小哥只是贪玩和童贞,并不是坏孩子。看见班主任愿意理解他的感情,替他承担压力,也是确实后悔,很快地就转心学习了。高考也甚是喜报,在年级前十都有清北一搏之力的省重点高校里,他考了全年级二十来名,非重点班第二。

然后大家才发现,他对感情的态度,和他两年来对学习的态度一样,都是认真的。他和徐小妹都报了浙大,最后也都去了浙大,尽管分数有差距不在一个系。浙大一直以来在本省招生两三千人,只要分数合适服从调剂,确实是个风险很低的选择。但尽管如此,依然有理由相信,小妹在两年的时间里对杨小哥的态度也有了不少转变,毕竟报志愿这事,也不是那么容易就能被小哥打听到的。

杨小哥用事实证明自己的态度后,在师生家长之间的声誉已经大为改观。但当大家都以为能成就一段姻缘时,四年过去,徐小妹保研,并在研究生期间和另一位同门博士师兄结了婚。杨小哥毕业以后则不知去向。

不知去向,只是对外人而言的。我父亲知道,我也知道,他是真的受了很大的刺激。徐小妹要的是一份现成的事业有成,让她可以不受风雨,满足于花艺茶道胭脂新妆,满足于官人四海觅封侯奴家相思如流水。博士师兄家境不错,工作高端,收入丰厚,毕竟是那时的博士,在那时的外企上班,相当高富帅。而杨小哥一如所有的学生一样,一穷二白,一无所有。可怜七年单相思,最初是无聊的笑话,最后是凄惨的笑话。

故事继续老套地发展,杨小哥受了刺激,下海经商,终成巨……其实没有成巨贾。跟很多小商小贩那样,起早贪黑,要说挣,比父亲确实多一些,但折算假期摊薄时薪,其实也没多多少。换句话说,挣了份辛苦钱而已。这种程度,是依然远远比不上那个并无此意却在事实上刺激到了他的博士先生的。

所以这个故事不是什么鸡汤文,徐小妹那边幸福指数继续涨,生下了双胞胎徐小小妹。杨小哥熬成了杨大哥,挣了钱,也成了家,有了自己的小小哥。两人的轨迹本应该就停在高一那刹那间心动的初见。

但这十几年来,事情开始各种奔放地发展。

第一件事是博士师兄不幸早亡。我并不清楚原因,杨大哥并不愿意细述。他只是说,徐小妹因为生的是双胞胎女儿,大概还有财产方面的原因,非常不受婆家待见,全靠夫君出面平衡。加之多年家庭主妇没有走动,在夫亡之后,婆家就基本断绝了关系,娘家也不来往了。要知道,法律并不保护不会伸张自己权益的人,小妹最后落得个孤儿寡女,靠不多的遗产过活。杨大哥接济了她好几年。

第二件事是杨大哥终于富了。他近几年卖车开 4S 店终于挣了不少钱,心一狠给徐小妹买了套房,接她们娘儿仨到自己身边。刚听时,我觉得这不就是初恋成小三么,虽然总会有生活琐事不易处理,但杨大哥自己乐意,也确是相思多年,算个大事,但不算奇事吧。后来才知道,事情要复杂得多。

第三件事是,杨大哥终于离婚了。这件事我很难评价,因为他接济徐小妹,更像是供一尊菩萨。肉体有没有出轨不知道,但精神出轨……我想说从杨小哥到杨大哥,他就没变过轨,如果说最初是爱,那十几年未见还能如此,说是信仰也不差了。杨夫人是好人,她主动提的离婚,也不让杨大哥净身出户,儿子也不小了可以面对这事。当然,这个我也只是电话听杨大哥说,不知道事实如何。

所以万事俱备,接下来杨大哥和徐小妹在一起了么?

还没,不过杨大哥确实重新谈起了恋爱,久违了十几年的恋爱,和两位徐小小妹。

你没看错,我电话听到这里也是懵逼的,杨大哥和徐小妹的双胞胎女儿一起谈起了恋爱。

我了个去!

两位小小妹还是挺喜欢杨大哥的,毕竟孤儿寡女时,待她们最好的确实就是杨叔叔,比自己的奶奶家好得多。随着年岁渐长,情窦初开,依赖之情也更浓了。杨大哥呢,眼里的两位小姑娘的身形,俨然就是自己魂牵梦萦十几年的当年那个小姑娘的身影,音容笑貌,举手投足,完全是一个模子出来的。区别只在于当年那姑娘不喜欢他,而现在的小姑娘确实喜欢他也待他很好。俩小姑娘比她们娘还好的一点是,虽然也风花雪月满脑幻想,但懂得适可而止懂得体谅。

妈的简直好太多了。这两妹子对杨大哥一片真心,也没有家长方面的问题——母亲多年艰辛以后变得懦弱而更加依赖别人,父亲早亡则正是她们沦落如此的原因之一。

从我电话里听见的声音的沙哑与颤抖来看,杨大哥也确实犹豫着。我想他现在的压力,未必比当年那时小。他实在是走投无路,才找到他尊敬的当年班主任,倾诉也好,忏悔也罢,无论是放弃还是坚持,他都需要更多的理由。

我父亲的观点是,他们不是一辈人,不应该在一起,这是不对的。当然,两个……更不对,但就算一个,也是不对的。

我的观点是,杨大哥喜欢的,是那个影子。这个影子被过去烙在了心里,一直没能填补,而现在终于找到了。两……三情相悦,先这么过着吧。如果他接济徐小妹但谈恋爱的妹子是别人家的女儿,就只是一般的老少恋,其实不算什么异常的事。

我不知道之后会怎么样。

第二个故事就简单多了。

我师弟。

也是高中恋爱,少年单纯少女巧思,虽然女孩子成绩差点,可师弟智商超群并不在乎,就像王思聪并不在乎交的朋友有没有钱一样。虽然师弟跑来北大,妹子去了三本,但两人依然你侬我侬,千里送炮,毕业后两人就结婚了。两家家长都很喜欢这对小夫妇,也都是中产家境门当户对。两家各出一套全款房车,一处省会赴任,一处老家抱孙,简直是人生赢家。

还没抱孙,人生赢家偷偷找我聊天了。

想离婚。

我说 QNMD 你离个毛你有啥不满的人家八分相貌九分身材我一想到你老婆就鸡儿邦硬亏 TM 她对你十分真心,你 TM 就是没有房贷闲的要不帮我凑个首付我 200 万首付只差 190 万了。

他说,你能理解我读书有感而发找她聊天说也许宋朝被灭是因为唐朝对外征服时把国家级的组织方式教给了北方游牧民族部落导致人家从部落级的散兵游勇变成了强大的国家军队反过来征服了中原结果她说我知道的匈奴都是坏人呀,然后我沉默了过一会想换个话题于是说哦这个观点有意思作者认为尽管自由贸易会使两个国家或地区都获益但由于这些收益只会分配到两国参与交易的部分民众而非全体国民所以实际上会导致资本输出国内的劳动力和劳动输出国内的资本方利益受损因此任何的自由贸易都会受到国家保护主义的反抗结果她说那 Lv 包为什么还是那么贵……

……时候的感受么?

我说我能。

我理解她是真爱她夫君也已经在尽力迎合她的夫君时的无奈,我也理解师弟真的无从找到倾泻他富余智力的渠道时的苦闷。

琴瑟和谐,真的不是那么容易。

其实要我说,师弟这个结,比杨大哥那个结,还难解得多。杨大哥那个结,不过是自己的心结,解不解,只看他一个人就够了。我们旁人,总还是愿意祝福的。而心结,即使无人帮助,迟早也会自解。

师弟这个结,我却觉得无解。

我说,我能劝解你的话,你自己也会说。

他说,是。

他说,羡慕您。

我说,其实和你一样。

他说,那你和我一样吗?

我说,一样。

他说,懂了。

聊天就此结束了,希望这个懂了,能再延续她几年的婚姻。至于师弟,只能说我越来越能理解,为什么有时天才们也会跑去向神佛寻求智慧了。

0%