使用Golang替换文件中的一行

huangapple go评论85阅读模式
英文:

Replacing a line within a file with Golang

问题

我是你的中文翻译助手,以下是你要翻译的内容:

我对Golang还不熟悉,正在尝试一些例子。目前,我想做的是逐行读取文件,并在满足某个条件时将其替换为另一个字符串。
用于测试的文件包含四行:

one
two
three
four

处理该文件的代码如下:

func main() {
    file, err := os.OpenFile("test.txt", os.O_RDWR, 0666)
    if err != nil {
        panic(err)
    }

    reader := bufio.NewReader(file)

    for {
        fmt.Print("Try to read ...\n")

        pos, _ := file.Seek(0, 1)
        log.Printf("Position in file is: %d", pos)
        bytes, _, _ := reader.ReadLine()

        if len(bytes) == 0 {
            break
        }

        lineString := string(bytes)

        if lineString == "two" {
            file.Seek(int64(-(len(lineString))), 1)
            file.WriteString("This is a test.")
        }

        fmt.Printf(lineString + "\n")
    }

    file.Close()
}

从代码片段中可以看出,我希望在从文件中读取到字符串"two"时,将其替换为"This is a test"。
为了获取文件中的当前位置,我使用了Go的_Seek_方法。
然而,问题是每次都会将最后一行替换为"This is a test",使得文件看起来像这样:

one
two
three
This is a test

通过检查将当前文件位置写入终端的打印语句的输出,我发现在__读取完第一行之后__,会得到以下输出:

2016/12/28 21:10:31 Try to read ...
2016/12/28 21:10:31 Position in file is: 19

所以在第一次读取之后,位置光标已经指向了文件的末尾,这就解释了为什么新字符串会被追加到末尾。有人能理解这里发生了什么,或者是什么导致了这种行为吗?

英文:

I'm new to Golang, starting out with some examples. Currently, what I'm trying to do is reading a file line by line and replace it with another string in case it meets a certain condition.
The file is use for testing purposes contains four lines:

one
two
three
four

The code working on that file looks like this:

<!-- language: golang -->

 func main() {

     file, err := os.OpenFile(&quot;test.txt&quot;, os.O_RDWR, 0666)

     if err != nil {
	   panic(err)
     }

     reader := bufio.NewReader(file)

      for {

	     fmt.Print(&quot;Try to read ...\n&quot;)

	     pos,_ := file.Seek(0, 1)
	     log.Printf(&quot;Position in file is: %d&quot;, pos)
	     bytes, _, _ := reader.ReadLine()

	     if (len(bytes) == 0) {
		    break
	     }

	     lineString := string(bytes)

	     if(lineString == &quot;two&quot;) {
		     file.Seek(int64(-(len(lineString))), 1)
		     file.WriteString(&quot;This is a test.&quot;)
	     }

	     fmt.Printf(lineString + &quot;\n&quot;)
   }

    file.Close()
 }

As you can see in the code snippet, I want to replace the string "two" with "This is a test" as soon as this string is read from the file.
In order to get the current position within the file I use Go's Seek method.
However, what happens is that always the last line gets replaced by This is a test, making the file looking like this:

one
two
three
This is a test

Examining the output of the print statement which writes the current file position to the terminal, I get that kind of output after the first line has been read:

2016/12/28 21:10:31 Try to read ...
2016/12/28 21:10:31 Position in file is: 19

So after the first read, the position cursor already points to the end of my file, which explains why the new string gets appended to the end. Does anyone understand what is happening here or rather what is causing that behavior?

答案1

得分: 3

读者不受file.Seek控制。你声明了读者为:reader := bufio.NewReader(file),然后你一次读取一行:bytes, _, _ := reader.ReadLine(),然而file.Seek不会改变读者正在读取的位置。

建议你阅读文档中关于ReadSeeker的内容,并切换到使用它。还有一个使用SectionReader的示例。

英文:

The Reader is not controller by the file.Seek. You have declared the reader as: reader := bufio.NewReader(file) and then you read one line at a time bytes, _, _ := reader.ReadLine() however the file.Seek does not change the position that the reader is reading.

Suggest you read about the ReadSeeker in the docs and switch over to using that. Also there is an example using the SectionReader.

答案2

得分: 3

除了错误的 seek 用法之外,困难之处在于你要替换的行与替换内容的长度不同。标准的做法是创建一个带有修改的新(临时)文件。假设这一步成功,就用新文件替换原始文件。

package main

import (
	"bufio"
	"io"
	"io/ioutil"
	"log"
	"os"
)

func main() {

	// 要修改的文件
	name := "text.txt"

	// 打开原始文件
	f, err := os.Open(name)
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	// 创建临时文件
	tmp, err := ioutil.TempFile("", "replace-*")
	if err != nil {
		log.Fatal(err)
	}
	defer tmp.Close()

	// 在从 f 复制到 tmp 的同时进行替换
	if err := replace(f, tmp); err != nil {
		log.Fatal(err)
	}

	// 确保成功写入临时文件
	if err := tmp.Close(); err != nil {
		log.Fatal(err)
	}

	// 关闭正在读取的文件
	if err := f.Close(); err != nil {
		log.Fatal(err)
	}

	// 用临时文件覆盖原始文件
	if err := os.Rename(tmp.Name(), name); err != nil {
		log.Fatal(err)
	}
}

func replace(r io.Reader, w io.Writer) error {
	// 使用 scanner 逐行读取
	sc := bufio.NewScanner(r)
	for sc.Scan() {
		line := sc.Text()
		if line == "two" {
			line = "This is a test."
		}
		if _, err := io.WriteString(w, line+"\n"); err != nil {
			return err
		}
	}
	return sc.Err()
}

对于更复杂的替换,我实现了一个可以替换正则表达式匹配的包。https://github.com/icholy/replace

import (
	"io"
	"regexp"

	"github.com/icholy/replace"
	"golang.org/x/text/transform"
)

func replace2(r io.Reader, w io.Writer) error {
	// 编译多行正则表达式
	re := regexp.MustCompile(`(?m)^two$`)

	// 创建替换转换器
	tr := replace.RegexpString(re, "This is a test.")

	// 在复制的同时进行转换
	_, err := io.Copy(w, transform.NewReader(r, tr))
	return err
}
英文:

Aside from the incorrect seek usage, the difficulty is that the line you're replacing isn't the same length as the replacement. The standard approach is to create a new (temporary) file with the modifications. Assuming that is successful, replace the original file with the new one.

package main
import (
&quot;bufio&quot;
&quot;io&quot;
&quot;io/ioutil&quot;
&quot;log&quot;
&quot;os&quot;
)
func main() {
// file we&#39;re modifying
name := &quot;text.txt&quot;
// open original file
f, err := os.Open(name)
if err != nil {
log.Fatal(err)
}
defer f.Close()
// create temp file
tmp, err := ioutil.TempFile(&quot;&quot;, &quot;replace-*&quot;)
if err != nil {
log.Fatal(err)
}
defer tmp.Close()
// replace while copying from f to tmp
if err := replace(f, tmp); err != nil {
log.Fatal(err)
}
// make sure the tmp file was successfully written to
if err := tmp.Close(); err != nil {
log.Fatal(err)
}
// close the file we&#39;re reading from
if err := f.Close(); err != nil {
log.Fatal(err)
}
// overwrite the original file with the temp file
if err := os.Rename(tmp.Name(), name); err != nil {
log.Fatal(err)
}
}
func replace(r io.Reader, w io.Writer) error {
// use scanner to read line by line
sc := bufio.NewScanner(r)
for sc.Scan() {
line := sc.Text()
if line == &quot;two&quot; {
line = &quot;This is a test.&quot;
}
if _, err := io.WriteString(w, line+&quot;\n&quot;); err != nil {
return err
}
}
return sc.Err()
}

For more complex replacements, I've implemented a package which can replace regular expression matches. https://github.com/icholy/replace

import (
&quot;io&quot;
&quot;regexp&quot;
&quot;github.com/icholy/replace&quot;
&quot;golang.org/x/text/transform&quot;
)
func replace2(r io.Reader, w io.Writer) error {
// compile multi-line regular expression
re := regexp.MustCompile(`(?m)^two$`)
// create replace transformer
tr := replace.RegexpString(re, &quot;This is a test.&quot;)
// copy while transforming
_, err := io.Copy(w, transform.NewReader(r, tr))
return err
}

答案3

得分: 1

OS包中有一个Expand函数,我相信它可以用来解决类似的问题。

解释:

file.txt

one
two
${num}
four

main.go

package main
import (
"fmt"
"os"
)
var FILENAME = "file.txt"
func main() {
file, err := os.ReadFile(FILENAME)
if err != nil {
panic(err)
}
mapper := func(placeholderName string) string {
switch placeholderName {
case "num":
return "three"
}
return ""
}
fmt.Println(os.Expand(string(file), mapper))
}

输出

one
two
three
four

此外,你可以创建一个配置文件(yml或json),并将数据填充到映射中,该映射可以用作查找表,用于存储占位符及其替换字符串,并修改mapper部分以使用该表从输入文件中查找占位符。

例如,映射将如下所示:

table := map[string]string {
"num": "three"
}
mapper := func(placeholderName string) string {
if val, ok := table[placeholderName]; ok {
return val
}
return ""
}

参考资料:

  • os.Expand文档:https://pkg.go.dev/os#Expand

<kbd>Playground</kbd>

英文:

OS package has Expand function which I believe can be used to solve similar problem.

Explanation:

file.txt

one
two
${num}
four

main.go

package main
import (
&quot;fmt&quot;
&quot;os&quot;
)
var FILENAME = &quot;file.txt&quot;
func main() {
file, err := os.ReadFile(FILENAME)
if err != nil {
panic(err)
}
mapper := func(placeholderName string) string {
switch placeholderName {
case &quot;num&quot;:
return &quot;three&quot;
}
return &quot;&quot;
}
fmt.Println(os.Expand(string(file), mapper))
}

output

one
two
three
four

Additionally, you may create a config (yml or json) and
populate that data in the map that can be used as a lookup table to store placeholders as well as their replacement strings and modify mapper part to use this table to lookup placeholders from input file.

e.g map will look like this,

table := map[string]string {
&quot;num&quot;: &quot;three&quot;
}
mapper := func(placeholderName string) string {
if val, ok := table[placeholderName]; ok {
return val
}
return &quot;&quot;
}

References:

<kbd>Playground</kbd>

huangapple
  • 本文由 发表于 2016年12月29日 04:16:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/41369106.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定