我有一个文本文件,想用这样的记录来解析:
===================
name: John Doe
Education: High School Diploma
Education: Bachelor's Degree
Education: Sun Java Certified Programmer
Age: 29
===================
name: Bob Bear
Education: High School Diploma
Age: 18
===================
name: Jane Doe
Education: High School Diploma
Education: Bachelor's Degree
Education: Master's Degree
Education: AWS Certified Solution Architect Professional
Age: 25
如您所见,此类文本文件中的fields
是固定的,但其中一些重复任意次数。记录由固定长度的====
分隔符分隔。
我将如何为此类问题编写解析逻辑?我在使用switch
来读取行的开头时就想到了,但是处理多个重复字段的逻辑使我感到困惑。
答案 0 :(得分:1)
解决此类问题的一种好方法是“分而治之”。也就是说,将整个问题分为较小的子问题,这些子问题易于管理,然后分别解决每个子问题。如果您进行了适当的计划,那么当您完成每个子问题时,您应该已经解决了整个问题。
首先考虑建模。该文档似乎包含记录列表,这些记录应称为什么?记录应包含哪些命名字段以及它们应具有哪些类型?您将如何在惯用语中代表他们?例如,您可能决定将每个记录称为Person
,其字段如下:
type Person struct {
Name string
Credentials []string
Age int
}
接下来,考虑一下解析函数的接口(签名)应该是什么样。它应该散发出很多人吗?它应该使用访客模式并在解析后立即释放人员吗?什么约束应该驱动答案?内存或计算时间限制重要吗?解析器的用户是否希望对解析工作进行任何控制,例如取消操作?他们是否需要元数据,例如文档中包含的记录总数?输入是否总是来自文件或字符串,可能来自HTTP请求或网络套接字?这些选择将如何驱动您的设计?
func ParsePeople(string) ([]Person, error) // ?
func ParsePeople(io.Reader) ([]Person, error) // ?
func ParsePeople(io.Reader, func visitor(Person) bool) error // ?
最后,您可以实现解析器以实现您决定的接口。一种简单的方法是逐行读取输入文件,并根据该行的内容执行操作。例如(用伪代码):
forEach line = inputFile.line
if line is a separator
emit or store the last parsed person, if present
create a new person to store parsed fields
else if line is a data field
parse the data
update the person with the parsed data
end
end
return the parsed records or final record, if emitting
上面的每一行伪代码代表一个子问题,该子问题应该比整体问题更容易解决。
答案 1 :(得分:-2)
编辑:添加关于为什么我只是发布程序作为答案的说明。
我正在提出一个非常简单的实现,以解析您在问题中给出的文本。您接受了数学答案,这是可以的。不过,我想在他的回答中添加一些反论点。基本上,该答案中的伪代码是我的答案中代码的不可编译版本,因此我们同意解决方案。
我不同意的是过度设计。我每天都要处理思想过度的人编写的代码。我敦促您不要考虑模式,内存和时间限制,否则将来谁可能想要这些。
访客模式?那几乎只是在解析编程语言时有用的东西,不要试图从这个问题构造一个用例。访客模式用于遍历其中包含不同类型事物的树。这里我们列出了所有相同的东西,而不是一棵树。
内存和时间限制?您正在解析5 GB的文本吗?那么这可能是一个真正的问题。但是,即使您这样做,也要始终先写最简单的东西。足够了。在我的整个职业生涯中,我只需要每年使用一次简单数组之外的其他东西或应用复杂算法。我仍然到处都看到无缘无故地使用复杂数据结构和算法的代码。这会使更改变得复杂,容易出错,有时最终会使速度变慢!不要使用可观察的列表抽象,只要它的内容发生更改,它就会通知所有观察者-但是,让我们添加一个更新锁定和解锁,以便我们可以控制何时不通知所有人...否!不要沿着那条路线走。用一片。做你的逻辑。使所有内容从上到下都易于阅读。我不想从A跳到B到C,追逐接口,跟随吸气剂,最终找到的不是具体的数据类型,而是另一个接口。那不是要走的路。
这就是为什么我的代码不导出任何内容的原因,它是一个独立的,可运行的示例,作为您具体问题的具体解决方案。您可以阅读它,很容易理解。它没有被大量评论,因为它不需要。这三个评论不是说明会发生什么,而是说明为什么会发生。其他所有内容都可以从代码本身中看出。我故意留下有关潜在错误的注释。您知道自己拥有哪种数据,那里没有行会触发此错误。不要编写代码来处理不可能发生的事情。如果将来有人在冒号后添加一行不带文本的行(请记住,没有人会这样做,不用担心),这会引发恐慌,将您指向该行,然后再添加if或something,你完成了。该代码比试图处理输入的各种不同的不存在的变化的程序更能证明未来。
我要强调的重点是:只写解决眼前问题所需的内容。除此之外,一切都使您的程序难以阅读和更改,它将未经测试且不必要。
话虽如此,这是我的原始答案:
https://play.golang.org/p/T6c51jSM5nr
package main
import (
"fmt"
"strconv"
"strings"
)
func main() {
type item struct {
name string
educations []string
age int
}
var items []item
var current item
finishItem := func() {
if current.name != "" { // handle the first ever separator
items = append(items, current)
}
current = item{}
}
lines := strings.Split(code, "\n")
for _, line := range lines {
if line == separator {
finishItem()
} else {
colon := strings.Index(line, ":")
if colon != -1 {
id := line[:colon]
value := line[colon+2:] // note potential bug if text has nothing after ':'
switch id {
case "name":
current.name = value
case "Education":
current.educations = append(current.educations, value)
case "Age":
age, err := strconv.Atoi(value)
if err == nil {
current.age = age
}
}
}
}
}
finishItem() // in case there was no separator at the end
for _, item := range items {
fmt.Printf("%s, %d years old, has educations:\n", item.name, item.age)
for _, e := range item.educations {
fmt.Printf("\t%s\n", e)
}
}
}
const separator = "==================="
const code = `===================
name: John Doe
Education: High School Diploma
Education: Bachelor's Degree
Education: Sun Java Certified Programmer
Age: 29
===================
name: Bob Bear
Education: High School Diploma
Age: 18
===================
name: Jane Doe
Education: High School Diploma
Education: Bachelor's Degree
Education: Master's Degree
Education: AWS Certified Solution Architect Professional
Age: 25`