본문 바로가기
프로젝트 일지/BMT

10. Go를 이용한 스크래퍼 리팩토링하기 (6) - S3 Downloader Uploader 구현 & strings 패키지를 활용한 최종 데이터 형식 변환 (feat. strings.Trim(), strings.Replace())

by 데브겸 2023. 8. 18.

이번 시간에 본격적으로 NaverCrawler 코드, 그 중 AWS S3 Downloader / Uploader 에 대해서 알아보겠다. 구현 과정에 대해 설명하면서 추가적으로 엘라스틱서치를 위한 데이터 변환에 대해서도 살짝 다뤄본다.

 

 

Amazon S3 사용 파트 코드 전체

type BucketBasics struct {
	S3Client *s3.Client
}

func HandleRequest(_ context.Context) (string, error) {
	start := time.Now()

	bucktBasics := AWSConfigure()
	
    // S3에서 subway_information.json을 다운받아 내용을 INFO 변수에 넣기
	INFO, err := S3Downloader(bucktBasics)
	checkErr(err)
    
    // 파일을 S3에 업로드
	err := S3Uploader(data, bucktBasics, finalFilename, fileTime)
	if err != nil {
		fmt.Printf("S3Uploader failed, %v", err)
	}
}

// S3에서 파일 다운로드 후 json 데이터를 파싱하여 golang 자료 구조에 맞게 변환
func S3Downloader(basics BucketBasics) (map[string][]map[string]interface{}, error) {
	result, err := basics.S3Client.GetObject(context.TODO(), &s3.GetObjectInput{
		Bucket: aws.String(os.Getenv("AWS_BUCKET_NAME")),
		Key:    aws.String("subway_information.json"),
	})
	if err != nil {
		log.Printf("Couldn't get object. Here's why: %v", err)
		return nil, err
	}

	defer result.Body.Close()

	body, err := io.ReadAll(result.Body)
	if err != nil {
		log.Printf("failed to read response body: %v", err)
		return nil, err
	}

	var INFO = map[string][]map[string]interface{}{}
	err = json.Unmarshal(body, &INFO)
	checkErr(err)
	return INFO, nil
}

// AWS S3 사용을 위한 credential 설정 & client 생성
func AWSConfigure() BucketBasics {
	staticProvider := credentials.NewStaticCredentialsProvider(
		os.Getenv("AWS_BUCKET_ACCESS_KEY"),
		os.Getenv("AWS_BUCKET_SECRET_KEY"),
		"")

	sdkConfig, err := config.LoadDefaultConfig(
		context.Background(),
		config.WithCredentialsProvider(staticProvider),
		config.WithRegion(os.Getenv("AWS_REGION")),
	)
	checkErr(err)

	s3Client := s3.NewFromConfig(sdkConfig)
	bucketBasics := BucketBasics{s3Client}

	return bucketBasics
}

// struct를 json 형태로 변환 후 makingFileName에서 나온 이름으로 S3에 파일 업로드
func S3Uploader(data []map[string]string, basics BucketBasics, finalFileName string, fileTime string) error {
	tmp, err := json.Marshal(data)
	if err != nil {
		log.Fatalf("JSON marshaling failed: %s", err)
	}
	// Elastic에 넣기 위해 데이터 형식 변경
	justString := string(tmp)
	content := strings.Replace(justString, "},{", "}\n{", -1)
	content = strings.Trim(content, "[]")

	// 바이트 스트림을 S3에 업로드
	_, err = basics.S3Client.PutObject(context.TODO(), &s3.PutObjectInput{
		Bucket: aws.String(os.Getenv("AWS_BUCKET_NAME")),
		Key:    aws.String(fileTime + "/" + finalFileName),
		Body:   strings.NewReader(content),
	})
	if err != nil {
		return fmt.Errorf("failed to upload, %v", err)
	}
	fmt.Println(finalFileName + "file successfully uploaded in S3")
	return nil
}

 

 

 


 

S3 Downloader 구현

 

AWS Credential Config 구현 방법을 토대로 대략적인 S3 SDK 사용하는 방법은 아래 포스트에 기재해두었다.

 

7. Go를 이용한 스크래퍼 리팩토링하기 (3) - AWS Credential Configure, S3 Uploader 구현하기 (feat. AWS SDK for G

저번 포스팅에서는 고루틴을 이용하여 19900개의 URL을 날려보고, 검색 결과를 goquery를 통해 가져와 저장하는 것까지 알아보았다. (링크: https://kyumcoding.tistory.com/62) 6. Go를 이용한 스크래퍼 코드 리

kyumcoding.tistory.com

 

위 코드와 마찬가지로 config.LoadDefaultConfig, credentials.NewStaticCredentialsProvider, s3.NewFromConfig를 통해bucketBasics를 만든다. 이후 buckerBasics를 활용하여 S3Downloader, S3Uploader를 구현하는 구조이다.

 

type BucketBasics struct {
	S3Client *s3.Client
}

func HandleRequest(_ context.Context) (string, error) {
	bucktBasics := AWSConfigure()

	// S3에서 파일 다운로드
	INFO, err := S3Downloader(bucktBasics)
	if err != nil {
		log.Fatalln(err)
	}
    
    // 파일을 S3에 업로드
	err := S3Uploader(data, bucktBasics, finalFilename, fileTime)
	if err != nil {
		fmt.Printf("S3Uploader failed, %v", err)
	}
}

 

 

S3Downloader의 경우 AWS 공식 문서 Go V2용 SDK를 사용하는 아마존 S3 예제의 '버킷에서 객체 가져오기' 파트를 참고하여 작성하였다.

 

공식 문서 예제 코드

// DownloadFile gets an object from a bucket and stores it in a local file.
func (basics BucketBasics) DownloadFile(bucketName string, objectKey string, fileName string) error {
	result, err := basics.S3Client.GetObject(context.TODO(), &s3.GetObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String(objectKey),
	})
	if err != nil {
		log.Printf("Couldn't get object %v:%v. Here's why: %v\n", bucketName, objectKey, err)
		return err
	}
	defer result.Body.Close()
	file, err := os.Create(fileName)
	if err != nil {
		log.Printf("Couldn't create file %v. Here's why: %v\n", fileName, err)
		return err
	}
	defer file.Close()
	body, err := io.ReadAll(result.Body)
	if err != nil {
		log.Printf("Couldn't read object body from %v. Here's why: %v\n", objectKey, err)
	}
	_, err = file.Write(body)
	return err
}

 

다만 나는 따로 객체의 내용을 파일로 저장하지 않고, 메모리 위에 올려두어 계속해서 사용해야 하기 때문에 os.Create 파트는 제외하여 구현했다. 객체의 내용을 INFO 변수에 담는 것으로 코드를 변경하였다.

 

basics.S3Client.GetObject를 통해 result에 GetObject 타입의 데이터를 받는다. (공식 문서 참고)

 

이후 io.ReadCloser 타입(Reader와 Closer 메소드를 지닌 인터페이스 - 공식 문서 참고)의 result.Body를 처리한다.

body에 들어있는 값을 다 옮긴 이후에는 body는 더 이상 필요 없으므로 close 메소드를 쓸 수 있도록 일부러 Reader와 Closer 둘 다 가지고 있는 인터페이스를 넣어놓은 것이 아닌가 생각함

io.ReadAll로 데이터 소스(result.Body)의 모든 byte를 담은 []byte를 리턴받아 body에 넣는다. (조금 더 자세히 알고 싶다면 go가 byte stream을 다루는 방법에 대하여 블로그 글 참고)

 

이후 json 패키지의 Unmarshal을 통해 []byte를 내가 원하는 형태(map[string][]map[string]interface{}{})로 변환하여 파라미터로 들어온 (이미 변수로 선언된)INFO에 직접 그 값을 넣는다. (그렇기 때문에 포인터 타입으로 파라미터를 입력한다) (Marshal 관련 조금 더 자세하게 알고 싶다면 Go에서 데이터를 변환하는 방법 블로그 글 참고)

 

 

내가 구현한 코드

// S3에서 파일 다운로드 후 json 데이터를 파싱하여 golang 자료 구조에 맞게 변환
func S3Downloader(basics BucketBasics) (map[string][]map[string]interface{}, error) {
	result, err := basics.S3Client.GetObject(context.TODO(), &s3.GetObjectInput{
		Bucket: aws.String(os.Getenv("AWS_BUCKET_NAME")),
		Key:    aws.String("subway_information.json"),
	})
	if err != nil {
		log.Printf("Couldn't get object. Here's why: %v", err)
		return nil, err
	}

	defer result.Body.Close()

	body, err := io.ReadAll(result.Body)
	if err != nil {
		log.Printf("failed to read response body: %v", err)
		return nil, err
	}

	var INFO = map[string][]map[string]interface{}{}
	err = json.Unmarshal(body, &INFO)
	checkErr(err)
	return INFO, nil
}

 

 


S3 Uploader 구현하기 & 엘라스틱서치(ElasticSearch)를 위한 데이터 변환 (feat. Strings 패키지)

S3 Uploader 구현 또한 아래 포스트에 자세하게 설명해두었다. 대략적인 내용은 비슷

 

7. Go를 이용한 스크래퍼 리팩토링하기 (3) - AWS Credential Configure, S3 Uploader 구현하기 (feat. AWS SDK for G

저번 포스팅에서는 고루틴을 이용하여 19900개의 URL을 날려보고, 검색 결과를 goquery를 통해 가져와 저장하는 것까지 알아보았다. (링크: https://kyumcoding.tistory.com/62) 6. Go를 이용한 스크래퍼 코드 리

kyumcoding.tistory.com

 

// struct를 json 형태로 변환 후 makingFileName에서 나온 이름으로 S3에 파일 업로드
func S3Uploader(data []map[string]string, basics BucketBasics, finalFileName string, fileTime string) error {
	tmp, err := json.Marshal(data)
	if err != nil {
		log.Fatalf("JSON marshaling failed: %s", err)
	}
	// Elastic에 넣기 위해 데이터 형식 변경
	justString := string(tmp)
	content := strings.Replace(justString, "},{", "}\n{", -1)
	content = strings.Trim(content, "[]")

	// 바이트 스트림을 S3에 업로드
	_, err = basics.S3Client.PutObject(context.TODO(), &s3.PutObjectInput{
		Bucket: aws.String(os.Getenv("AWS_BUCKET_NAME")),
		Key:    aws.String(fileTime + "/" + finalFileName),
		Body:   strings.NewReader(content),
	})
	if err != nil {
		return fmt.Errorf("failed to upload, %v", err)
	}
	fmt.Println(finalFileName + "file successfully uploaded in S3")
	return nil
}

 

다만 전과 다른 점은 ElasticSearch에 넣기 위해 데이터 형식을 한 번 더 변경한다는 점이다. 이전에는 (위 코드를 예시로 들자면) data 변수에 있는 데이터를 json.marshal 하여 그것을 그대로 S3 client의 Body에 넣어 업로드 하였다. 반면 이번에는 데이터 형식을 아래의 코드로 한 번 더 변환하고 그것을 S3에 업로드 한다.

 

데이터 형식 변경 이전과 이후는 다음과 같다

// 변경 이전 데이터 예시
[{"arriveTime":"05:38:00","inOutTag":"1","lineNum":"1호선","stationNm":"덕정역","weekTag":"1"},{"arriveTime":"05:31:00","inOutTag":"2","lineNum":"1호선","stationNm":"덕정역","weekTag":"1"},{"arriveTime":"06:02:00","inOutTag":"1","lineNum":"1호선","stationNm":"덕정역","weekTag":"1"},{"arriveTime":"06:03:00","inOutTag":"2","lineNum":"1호선","stationNm":"덕정역","weekTag":"1"}]

// 변경 이후 데이터 예시
{"arriveTime":"05:38:00","inOutTag":"1","lineNum":"1호선","stationNm":"덕정역","weekTag":"1"}
{"arriveTime":"05:31:00","inOutTag":"2","lineNum":"1호선","stationNm":"덕정역","weekTag":"1"}
{"arriveTime":"06:02:00","inOutTag":"1","lineNum":"1호선","stationNm":"덕정역","weekTag":"1"}
{"arriveTime":"06:03:00","inOutTag":"2","lineNum":"1호선","stationNm":"덕정역","weekTag":"1"}

 

변경 방법은 아래와 같다

  1. 우선 []map[string]string 형식의 data를 json.Marshal을 통해 []byte 형태로 변환하고 tmp에 넣는다
  2. sting()을 사용하여 []byte 형태의 데이터를 문자열 형식으로 바꿔준다 (조금 더 공부해보니 fmt.Sprintf 등을 활용하는 것도 가능할듯 - 참고자료1, 참고자료2)
  3. strings 패키지의 Replace를 활용하여 {}사이에 쉼표를 개행으로 변경해준다 (변경 횟수 파라미터에 -1을 넣으면 모든 단어를 찾아 변경 - 참고자료 )
  4. strings 패키지의 Trim을 활용하여 데이터 맨앞맨뒤 []를 삭제하고 최종 변경 데이터를 content에 담는다 - 참고자료
  5. content는 문자열이고, S3 Client의 Body에는 io.Reader 인터페이스로 와야 하기 때문에 strings.NewReader를 활용하여 Reader 타입으로 변환한다 

 

코드는 아래와 같다.

tmp, err := json.Marshal(data) // data []map[string]string
if err != nil {
	log.Fatalf("JSON marshaling failed: %s", err)
}

// Elastic에 넣기 위해 데이터 형식 변경
justString := string(tmp)
content := strings.Replace(justString, "},{", "}\n{", -1)
content = strings.Trim(content, "[]")

// 바이트 스트림을 S3에 업로드
_, err = basics.S3Client.PutObject(context.TODO(), &s3.PutObjectInput{
	Bucket: aws.String(os.Getenv("AWS_BUCKET_NAME")),
	Key:    aws.String(fileTime + "/" + finalFileName),
	Body:   strings.NewReader(content),
})
if err != nil {
	return fmt.Errorf("failed to upload, %v", err)
}