Skip to main content
  1. Code Space/

有趣的算法题: 嵌套字段解析

·458 words·3 mins

题目如下: 解析一个文件,
文件格式是一行一行,
每行有字段,字段可以加引号。
字段里可以套字段。
如果被嵌套的字段有引号,引号要变成双重的,表示转义,
比如 “hello”,要变成 “…““hello””….”

2,John,45,"足球,摄影",New York
3,Carter Job,33,"""健身"",远足","河北,石家庄"
4,Steve,33,"大屏幕164""","DC""Home"""
5,"Jul,y","",,Canada
John,33,"足球,摄影",New York
John,33,"足球,""摄影",New York

要解析成如下

2 | John | 45 | 足球,摄影 | New York
3 | Carter Job | 33 | "健身",远足 | 河北,石家庄
4 | Steve | 33 | 大屏幕164 | DC"Home"
5 | Jul,y |  |  | Canada
John | 33 | 足球,摄影 | New York
John | 33 | 足球,摄影 | New York

这个题目没啥现实意义。
但是解答的时候却很有启发。

看懂了题目后,第一时间我想到的是栈,
因为嵌套结构,那么标识符号一定是成对的,所以push/pop刚好可以解决。
管他套几层括号转义,那还不等价于一个括号对嘛。
结果if else写了一堆,
然后我意识到一个问题,解析的时候应该分成几层状态,
一层是没引号的普通字段,一层是进入括号了,最后一层是进入嵌套括号了。

写完后忽然意识到,这是个自动机,以前写算法从没写过这种。
但是以前实现HTTP协议解析,也是这么搞的,
nats源码里,基于CRLF的状态解析,也是这么搞的。

其间,第一种解法,写到一半,20分钟已经过去了,可能好久没写算法了,也可能是水平不行。
一个教训就是,通常想到一个解法,即使写到一般,发现这样不够简单优雅,也要写下去。
不然时间不一定够。 这时候新解法的很多细节其实还没想清楚。

以下是解答:

package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"strings"
)

const (
	CR = "\r"
	LF = "\n"
)

func main() {
	src, err := os.Open("src.txt")
	if err != nil {
		panic(err)
	}
	defer src.Close()
	dst, err := os.Create("dst.txt")
	if err != nil {
		panic(err)
	}
	defer dst.Close()
	parseFile(src, dst)
}

func parseFile(r io.Reader, w io.Writer) {
	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		fields := parseLine(scanner.Text())
		resu := strings.Join(fields, CR)
		fmt.Println(strings.Join(fields, " | "))
		w.Write([]byte(resu))
		w.Write([]byte(LF))
	}
	if err := scanner.Err(); err != nil {
		fmt.Fprintln(os.Stderr, "读取错误:", err)
	}
}

func parseLine(s string) []string {

	// 输出结果
	rsu := []string{}

	// field的头指针。
	start := 0

	// 当前解析的状态
	// -1 表示没有开始解析,或者解析结束了。
	// 0 表示 这个 field 没有 `"` 开头
	// 1 表示 这个 field 有 `"` 开头
	// 2 表示 这个 field 处在 子field中, 类似这种: `"abc""dd`
	status := -1 // 0 没有"开头, 1,有"开头但是不在字段中, 2. 在子field中
	// 可以用 go 里的蹩脚 枚举,哈哈哈

	for i := 0; i < len(s); i++ {
		switch status {
		case -1:
			if i == start {
				if s[i] == '"' {
					start = i + 1
					status = 1
				} else if s[i] == ',' {
					// 防止空字段。
					status = -1
					start = i + 1
				} else {
					status = 0
				}
				rsu = append(rsu, "")
			}
		case 0:
			// ,xxx,
			if s[i] == ',' {
				rsu[len(rsu)-1] += s[start:i]
				start = i + 1
				status = -1
				continue
			}
			if i == len(s)-1 {
				rsu[len(rsu)-1] += s[start:]
			}
			if s[i] == '"' {
				// panic("文件错误")
			}
		case 1:
			// "xxxx"
			if s[i] == '"' {
				// "xxxx""
				if i+1 < len(s) && s[i+1] == '"' {
					status = 2
				}
				// "xxxx",
				if (i+1 < len(s) && s[i+1] == ',') || i == len(s)-1 {
					status = -1
				}
				rsu[len(rsu)-1] += s[start:i]
				// 不管是 `",` 还是 `""` 都跳过
				start = i + 2
				i++
			}
		case 2:
			// "xx""xxx""
			if s[i] == '"' {
				if i+1 < len(s) && s[i+1] == '"' {
					status = 1
					rsu[len(rsu)-1] += s[start-1 : i+1]
				}
				// "xx""xxx",
				if (i+1 < len(s) && s[i+1] == ',') || i == len(s)-1 {
					status = -1
					rsu[len(rsu)-1] += s[start:i]
				}
				start = i + 2
				i++
			}
		}
	}
	return rsu
}