본문 바로가기

Go

[묘공단] Tucker의 Go 언어 프로그래밍 26장 : 단어검색프로그램

  • 목적 
    • 여러 텍스트파일에서 단어를 검색한다.
  • 사용법 
    • find 찾을단어 대상텍스트파일
  • 주요패키지
    • os, path/filepath, strings, bufio

 

 

 

  • 파일 목록을 가져오는 함수 

https://pkg.go.dev/path/filepath

 

  • 아래소스는 실행할때 인수를 받아서, 해당 인수에 해당하는 파일목록을 출력하는 프로그램이다.
//ch26/ex26.1//ex26.1.go
package main

import (
	"fmt"
	"os"
	"path/filepath"
)

func main() {
	if len(os.Args) < 3 { // ❶ 실행 인수 개수 확인
		fmt.Println("2개 이상의 실행 인수가 필요합니다. ex) ex26.1 word filepath")
		return
	}

	word := os.Args[1] // ❷ 실행 인수 가져오기
	files := os.Args[2:]
	fmt.Println("찾으려는 단어:", word)
	PrintAllFiles(files)
}

func GetFileList(path string) ([]string, error) {
	return filepath.Glob(path)
}

func PrintAllFiles(files []string) {
	for _, path := range files {
		filelist, err := GetFileList(path) // ❸ 파일목록 가져오기
		if err != nil {
			fmt.Println("파일 경로가 잘못되었습니다. err:", err, "path:", path)
			return
		}
		fmt.Println("찾으려는 파일 리스트")
		for _, name := range filelist {
			fmt.Println(name)
		}
	}
}

 

 

 

 

 

 

 

  • 아래의 소스는 파일을 열어서 라인별로 읽어, 화면에 출력하는 소스이다. 
// ch26/ex26.2//ex26.2.go
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	if len(os.Args) < 2 { // ❶ 실행 인수 개수 확인
		PrintFile("hamlet.txt")
		return
	}

	filename := os.Args[1] // ❷ 실행 인수 가져오기
	PrintFile(filename)
}
func PrintFile(filename string) {
	file, err := os.Open(filename) // ❶ 파일 열기
	if err != nil {
		fmt.Println("파일을 찾을 수 없습니다. ", filename)
		return
	}
	defer file.Close() // ❷ 함수 종료 전에 파일 닫기

	scanner := bufio.NewScanner(file) // ❸ 스캐너를 생성해서 한 줄씩 읽기
	for scanner.Scan() {
		fmt.Println(scanner.Text())
	}
}

 

  • 아래는 test.txt파일의 내용이다.
THE TRAGEDY OF HAMLET, PRINCE OF DENMARK
by William Shakespeare
Fort. Let us haste to hear it

 

  • 아래의 명령을 사용하여 해당 파일의 내용을 한줄씩 읽어서 출력한다. 

 

 

 

 

 

  • 아래는 완성된 소스이다. 파일리스트의 파일을 모두 검색하여 해당 단어에 맞는 줄을 출력해준다.
    • LineInfo : 라인번호와 문자열을 저장하는 구조체
    • FindInfo : 파일명과 Lineinfo배열 
    • findInfos = append(findInfos, FindWordInAllFiles(word, path)...) 해당 함수에서 단어를 검색하여 findInfos 넣는다.
    • strings.Contains()함수를 사용하여 단어를 찾는다.
    •  

https://pkg.go.dev/strings@go1.22.1#Contains

//ch26/ex26.3/ex26.3.go
package main

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

// 찾은 라인 정보
type LineInfo struct { // ❶ 찾은 결과 정보
	lineNo int
	line   string
}

// 파일 내 라인 정보
type FindInfo struct {
	filename string
	lines    []LineInfo
}

func main() {
	if len(os.Args) < 3 {
		fmt.Println("2개 이상의 실행 인수가 필요합니다. ex) ex26.3 word filepath")
		return
	}

	word := os.Args[1] // ❷ 찾으려는 단어
	files := os.Args[2:]
	findInfos := []FindInfo{}
	for _, path := range files {
		// ❸ 파일 찾기
		findInfos = append(findInfos, FindWordInAllFiles(word, path)...)
	}

	for _, findInfo := range findInfos {
		fmt.Println(findInfo.filename)
		fmt.Println("--------------------------------")
		for _, lineInfo := range findInfo.lines {
			fmt.Println("\t", lineInfo.lineNo, "\t", lineInfo.line)
		}
		fmt.Println("--------------------------------")
		fmt.Println()
	}
}

func GetFileList(path string) ([]string, error) {
	return filepath.Glob(path)
}

func FindWordInAllFiles(word, path string) []FindInfo {
	findInfos := []FindInfo{}

	filelist, err := GetFileList(path) // ❶ 파일 리스트 가져오기
	if err != nil {
		fmt.Println("파일 경로가 잘못되었습니다. err:", err, "path:", path)
		return findInfos
	}

	for _, filename := range filelist { // ❷ 각 파일별로 검색
		findInfos = append(findInfos, FindWordInFile(word, filename))
	}
	return findInfos
}

func FindWordInFile(word, filename string) FindInfo {
	findInfo := FindInfo{filename, []LineInfo{}}
	file, err := os.Open(filename)
	if err != nil {
		fmt.Println("파일을 찾을 수 없습니다. ", filename)
		return findInfo
	}
	defer file.Close()

	lineNo := 1
	scanner := bufio.NewScanner(file) // ❸ 스캐너를 만듭니다.
	for scanner.Scan() {
		line := scanner.Text()
		if strings.Contains(line, word) { // ❹ 한 줄씩 읽으면 단어 포함 여부 검색
			findInfo.lines = append(findInfo.lines, LineInfo{lineNo, line})
		}
		lineNo++
	}
	return findInfo
}

 

 

 

 

 

 

 

 

 

 

  • 위의소스는 싱글스레드라서 파일갯수가 많을수록 시간이 걸린다.
  • 아래 소스는 해당 문제를 개선하기 위하여 스레드를 사용하였다. 
  • go FindWordInFile(word, filename, ch) 으로 고루틴을 실행하고, 
  • 작업이 완료되면 ch <- findInfo 으로 결과를 채널로 넣는다.
//ch26/ex26.4/ex26.4.go
package main

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

type LineInfo struct {
	lineNo int
	line   string
}

type FindInfo struct {
	filename string
	lines    []LineInfo
}

func main() {
	if len(os.Args) < 3 { //
		fmt.Println("2개 이상의 실행인자가 필요합니다. ex) ex26.4 word filepath")
		return
	}

	word := os.Args[1]
	files := os.Args[2:]
	findInfos := []FindInfo{}
	for _, path := range files {
		findInfos = append(findInfos, FindWordInAllFiles(word, path)...)
	}

	for _, findInfo := range findInfos {
		fmt.Println(findInfo.filename)
		fmt.Println("--------------------------------")
		for _, lineInfo := range findInfo.lines {
			fmt.Println("\t", lineInfo.lineNo, "\t", lineInfo.line)
		}
		fmt.Println("--------------------------------")
		fmt.Println()
	}
}

func FindWordInAllFiles(word, path string) []FindInfo {
	findInfos := []FindInfo{}
	filelist, err := filepath.Glob(path) // 실행인자 가져오기
	if err != nil {
		fmt.Println("파일 경로가 잘못되었습니다. err:", err, "path:", path)
		return findInfos
	}

	ch := make(chan FindInfo)
	cnt := len(filelist)
	recvCnt := 0

	for _, filename := range filelist {
		go FindWordInFile(word, filename, ch) // ❶ 고루틴 실행
	}

	for findInfo := range ch {
		findInfos = append(findInfos, findInfo) // ❷ 결과 수집
		recvCnt++
		if recvCnt == cnt {
			// all received
			break
		}
	}
	return findInfos
}

func FindWordInFile(word, filename string, ch chan FindInfo) {
	findInfo := FindInfo{filename, []LineInfo{}}
	file, err := os.Open(filename)
	if err != nil {
		fmt.Println("파일을 찾을 수 없습니다. ", filename)
		ch <- findInfo
		return
	}
	defer file.Close()

	lineNo := 1
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := scanner.Text()
		if strings.Contains(line, word) {
			findInfo.lines = append(findInfo.lines, LineInfo{lineNo, line})
		}
		lineNo++
	}
	ch <- findInfo
}