我将使用 HTML 作为示例。但它可以是任何语言的任何编程代码。这种情况经常出现,处理文字代码进行搜索和替换会很有用。
这是一个例子。
我想在数千个 .html 文件中替换以下内容:
<h2 class="et_tophat">
<a href="http://example.com">example.com</a>
</h2>
有了这个:
<div class="tophat" id="myHeader">
<A HREF="http://newexample.com">NewExample</A>
</div>
如何才能做到这一点而不必手动编写反斜杠和转义符。似乎在 Linux 命令(如 Perl 等)上可能存在我不熟悉的实用程序或参数,用于从输入文件中读取搜索示例并从另一个输入文件中读取替换字符串,然后对其进行处理以在许多文件中全局进行此更改?
答案1
以下是使用 XML 解析器的答案:xmlstarlet
给定此输入文件:
<html>
<h2 class="et_tophat">
<a href="http://example.com">example.com</a>
</h2>
<h2 class="et_tophat">
<a href="http://example.com">example2.com</a>
</h2>
<div>
<h2 class="et_tophat">
<a href="http://example.com">example.com</a>
</h2>
</div>
</html>
您可以h2
使用此命令更新具有所列属性的所有元素:
xmlstarlet ed -O \
-r '//h2[@class = "et_tophat"]/a[@href = "http://example.com" and text()="example.com"]/..' -v 'TO_BE_CHANGED' \
-u '//TO_BE_CHANGED/@class' -v 'tophat' \
-u '//TO_BE_CHANGED/a/@href' -v 'http://newexample.com' \
-u '//TO_BE_CHANGED/a' -v 'NewExample' \
-r '//TO_BE_CHANGED' -v 'div' file
输出结果如下:
<html>
<div class="tophat">
<a href="http://newexample.com">NewExample</a>
</div>
<h2 class="et_tophat">
<a href="http://example.com">example2.com</a>
</h2>
<div>
<div class="tophat">
<a href="http://newexample.com">NewExample</a>
</div>
</div>
</html>
该命令的一些解释:
这个想法是在 xpath 中递归地用另一个元素替换一个元素。
因此,每次找到正确的元素(第一个-r
和-v
选项)时,其名称都会更改为虚拟名称TO_BE_CHANGED
。
接下来将里面的元素和属性改为正确的名称(选项-u
和-v
)。
最后进行的更改是将虚拟名称重命名为您想要的真实名称,例如<div>
。
答案2
就我个人而言,我会使用 Vim 来完成这项任务。作为一款文本编辑器,就地保存文件的工作流程对它来说非常自然。匹配多行字符串也相当容易。而且 Vim 还有多种“风格”的正则表达式,这使得转义固定字符串并将其转换为匹配模式变得很容易。
我将逐步描述如何完成此操作。
首先,我将两个字符串(模式和替换)捕获到两个寄存器中,这样我就可以使用表达式对它们进行操作。
将两者粘贴到文档中。然后使用 启动线性可视模式V
,选择模式一(带有 example.com 的模式),并使用 将其拖入“a”寄存器"ay
。
然后使用 启动线性视觉模式V
,选择替换的那个(带有 newexample.com 的那个)并使用 将其拉入寄存器“b” "by
。
好了,现在我们需要将“a”寄存器转换为匹配模式。我们将使用“非常无魔法”正则表达式,可以通过 来选择\V
,因为这样我们只需要转义反斜杠本身和匹配分隔符(即正斜杠)。我们将使用escape(@a,'\/')
来转义它们。
我们还需要将文字换行符替换为文字\n
字符串,这是匹配换行符的序列。我们可以使用 进行替换substitute()
,将实际的换行符与 匹配\n
,并将其替换为\\n
(替换时反斜杠需要转义)。
最后,我们可以将模式固定到一行的开头,使用\^
(使用“very nomagic”模式时需要反斜杠。)
综合起来,我们的模式是:
'\V\^'.substitute(escape(@a,'\/'),'\n','\\n','g')
(我知道这看起来很难。但是一旦你练习了,它就会变得更容易。在开发这些模式时使用'incsearch'
和'hlsearch'
也很有帮助,因为你可以直观地看到你正在选择什么。)
对于替换方,我们只需要寄存器“b”的值。我们可以使用\=
在替换端输入一个表达式,并且@b
是寄存器“b”的值。所以我们的替换很简单:
'\=@b'
现在我们可以组装:s/.../.../
命令了。我们将使用%
作为范围(在文件中的任何位置查找模式)和/e
修饰符,因此当找不到模式时它会忽略失败。把它们放在一起:
'%s/\V\^'.substitute(escape(@a,'\/'),'\n','\\n','g').'/\=@b/e'
这仍然是一个字符串,因此下一步是使用:execute
,它将字符串作为 Vim 命令(更准确地说是 Ex 命令)执行。此外,在这一步,我们将运行:update
,如果文件被修改,则保存文件(如:w
)。这将允许我们在批量命令中运行此命令并转到下一个文件。在 Vim 中,我们使用|
作为命令分隔符。到目前为止:
:execute '%s/\V\^'.substitute(escape(@a,'\/'),'\n','\\n','g').'/\=@b/e'|update
(边栏:这部分可以抽象为 Vim 函数,使用固定字符串进行搜索和替换。它本身可重复使用且有用。我不会绕弯子,因为这篇文章已经很长了,我将使用更长的命令行,因为它在 Vim 中也可以正常工作。)
上面的命令可以应用于单个文件并且它将应用修改,因此下一步是将其应用于数千个 HTML 文件。
如果我们计划在每一个我们可以使用树中的 HTML 文件(假设其中大多数或很大一部分将包含搜索到的模式):args
并:argdo
执行批量操作。我们首先:
:args **/*.html
html
这将构建当前目录下具有扩展名的所有文件的列表,并递归到所有子目录。
然后我们可以使用:argdo
将我们的替代命令应用到其中的每一个:
:argdo execute '%s/\V\^'.substitute(escape(@a,'\/'),'\n','\\n','g').'/\=@b/e'|update
请注意,/e
和|update
在此步骤中非常重要,因为我们希望在移动到下一个文件之前保存更改,并且我们不希望 Vim 在未找到某些文件时抱怨未找到匹配项。
或许每一个 *.html
文件太多,我们没有好的路径表达式来缩小范围。在这种情况下,也许我们可以使用:vimgrep
而是(:vimgrep /example\.com/ **/*.html
也许用?)或:grep
或插件,例如ack.vim
,在这种情况下,我们将在“错误列表”中得到结果,我们可以使用:cfdo
进行批量修改。
使用 Vim 完成这项任务的一大优点是它非常灵活。它可以快速迭代并随时更改需求。您可以获得大量视觉反馈(毕竟它是一个文本编辑器!),因此从这个意义上讲,它是一种比编写 Perl 或 Python 脚本来执行此替换更具交互性的方法。
缺点可能是复杂性。这些命令很快就会变得相当复杂。如果你对 Vim 了如指掌,这会变得更容易,但学习难度相当大。无论如何,我希望这篇文章至少能激励你更多地了解 Vim,以便用它来解决这种批量修改,这是 Vim 的强项。