1. 문제 인식
대학교 시절 그리고 현재 일본의 IT기업에서 일을 하는 지금까지도
일본어 또는 한국어로 된 문서를 열 때 문자가 깨지는 경험을 자주 했습니다.
문자가 깨지는 이 현상을 일본어로는 文字化け<もじばけ>라고 합니다.
문자 깨짐 현상은 일본에서 IT 쪽 업무를 보시게 된다면 어떤 방식으로든 경험하시게 될 것이라고 생각합니다.
저의 경우 타사 API와 통신을 하는 API를 구현할 때 타사 API의 응답의 값의 인코딩이 우리 회사에서 자주 쓰던 방식이 아니라던지,
운영부서 사람들이 제대로 된 인코딩 방식으로 파일을 열지 못해서 분석용 데이터를 활용하지 못해서
문자 깨짐 현상의 해결 방법에 대해 질문이 들어오는 등의 경험을 했습니다.
해결방법은 제대로 된 방식의 인코딩으로 파일을 열면 파일의 내용을 보는데 지장이 없습니다.
아래 사진은 'EUC-JP'방식으로 인코딩 된 파일 UTF-8로 열었기 때문에 문자 깨짐 현상이 일어났습니다.
저는 주로 비주얼 스튜디오 코드라는 툴을 사용해 문자 깨짐 현상을 해결합니다.
위의 방법을 사용하면 아주 쉽게 문자 깨짐 현상으로 제대로 읽을 수 없었던 문서의 내용을 확인할 수 있습니다.
그런데 파일이 엄청 많아서 수십 개, 수백 개가 넘어버린다면 파일을 열다가 귀찮아질게 뻔합니다.
문서의 원래 인코딩도 알아야 하고, 툴에서 변환 버튼 누르는 게 너무 귀찮을 것 같습니다.
OS의 기본으로 설정된 인코딩을 바꾼다던지 해서 대응하는 방법도 있겠지만,
개발자가 아닌 동료라면 설명하다가 지쳐버릴게 뻔하니, 문자 깨짐 현상이 발생하지 않는 인코딩 방식으로 파일을 일괄적으로 변환해서 줘버리면 설명할 필요도 없도 저도 동료도 편할 거라고 생각했습니다.
위에서 주저리주저리 이야기한 것이 이번 토이 프로젝트의 시작 이유입니다.
2. 서비스 개요
- 파일을 원하는 형식의 인코딩으로 변환해 주는 프로그램.
ファイルを欲しいタイプのエンコーディングに変換してくれるプログラム。
※ 실행 결과 実行結果
3. 요구 조건 要求条件
- 입력된 파일을 원하는 형식의 인코딩으로 변환하여 파일로 출력한다.
入力されたファイルを欲しいタイプのエンコーディングに変換してファイルで出力する。
- 복수의 파일을 동시에 인코딩 가능. 複数のファイルを同時にエンコーディング可能。
- 지정한 디렉터리의 파일을 일괄적으로 변환 가능. 指定したディレクトリのファイルを一括的に変換可能。
개발환경 開発環境
OS : Windows 10
사용언어 : Java; AdoptOpenJDK 11
운영환경 運営環境
OS : Amazon Linux (EC2)
사용언어 : Java; AdoptOpenJDK 11
4. 상세 사양 詳細仕様
1) 구동 변수 입력
-f ${파일 경로} ${인코딩 형식} / -f ${ファイルパス} ${エンコーディングタイプ}
: 지정된 파일 하나를 지정된 인코딩 형식으로 변경하여 출력한다. 인코딩 지정이 없을 경우 변환 인코딩의 기본값 UTF-8이다.
: 指定のファイル1個を指定したエンコーディングタイプに変換して出力する。エンコーディングタイプを指定してない場合、エンコーディングタイプのデフォルト値はUTF-8にする。
-l ${파일리스트}. csv ${인코딩 형식} / -l ${ファイルパス} ${エンコーディングタイプ}
: 파일리스트에 기재된 파일을 지정된 인코딩 형식으로 변경하여 출력한다. 인코딩 지정이 없을 경우 변환 인코딩의 기본값 UTF-8이다.
: ファイルリストに記載されているファイルを指定したエンコーディングタイプに変換して出力する。エンコーディングタイプを指定してない場合、エンコーディングタイプのデフォルト値はUTF-8にする。
-d ${디렉터리} ${인코딩 형식} / -d ${ディレクトリ} ${エンコーディングタイプ}
: 지정된 파일의 어떤 방식으로 인코딩 되어있는지 표준 출력으로 표시한다.
2) 구동 변수의 유효성 검사
$1 : $1의 값이 -f, -l, -d에 해당하지 않으면 에러코드 101로 프로그램을 종료한다.
: $1の値が -f, -l, -dどちらかではない場合はエラーコード101でプログラムを終了する。
$2
- ※ 공통 : 입력값이 Null 또는 공백, 아래의 조건에 일치하지 않는 경우 에러코드 102로 프로그램을 종료한다.
※ 共通 : 入力値がヌルまたは空白、下記の条件に一致しない場合はエラーコード102でプログラムを終了する。
- $1이 -f인 경우 : 변환하고 싶은 파일의 절대 경로를 입력한다.
変換したいファイルの絶対パスを入力する。
- $1이 -l인 경우 : 변환하고 싶은 파일의 절대 경로가 기재된 파일리스트(.csv파일)의 절대경로를 입력한다.
変換したいファイルの絶対パスが記載されたファイルリスト(.csvファイル)の絶対パスを入力する。
- $1이 -d인 경우 : 변환하고자 하는 파일들이 존재하는 디렉터리의 절대 경로를 입력한다.
変換したいファイル群が存在するディレクトリの絶対パスを入力する。
$3 : 변환하고 싶은 인코딩 방식을 입력한다. 인코딩 지정이 없을 경우 변환 인코딩의 기본값 UTF-8로 설정한다.
変換したいエンコーディングタイプを入力する。エンコーディングタイプ指定がない場合、変換エンコーディングタイプのデフォルト値はUTF-8に設定する。
3) 구동 변수별 처리
3-1) $1이 -f의 경우
- $2의 값으로 입력받은 파일의 존재 여부를 확인한다. $2の値で入力されたファイルの存在有無を確認する。
- 파일의 인코딩 방식을 확인한다. ファイルのエンコーディングタイプを確認する。
- 입력받은 원본 파일의 데이터를 읽어와 파일의 내용을 지정된 형식의 인코딩으로 변환한다. 入力された原本ファイルのデータを読込み、ファイルの内容を指定のエンコーディングタイプに変換。
- 변환된 데이터를 결과 파일에 출력한다. (원본파일은 보존한다.) 変換データを結果ファイルに出力する。
3-2) $1이 -l의 경우
- $2의 값으로 입력받은 파일리스트를 읽어온다. $2の値で入力されたファイルリストを読込。
- 파일리스트에 기재된 파일의 수만큼 3-1)의 처리를 반복한다. ファイルリストに記載されたファイル数分 3-1)の処理を繰り返す。
3-3) $1이 -d인 경우
- $2의 값으로 입력받은 디렉터리 안의 파일리스트를 읽어온다. $2の値で入力されたディレクトリのファイルリストを読込。
- 파일 리스트의 파일수만큼 3-1)의 처리를 반복한다. ァイルリストのファイル数分 3-1)の処理を繰り返す。
5. 순서도
6. 소스코드
package tools;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Properties;
import org.mozilla.universalchardet.UniversalDetector;
public class EncodingTranslator {
private String OUTPUT_DIR = "";
private String NEW_LINE = "";
private String SEPARATOR = "";
private String REGEX_EXT_FORMAT = "";
private EncodingTranslator() {}
public static EncodingTranslator getInstance() {
return EncodingTranslatorHolder.INSTANCE;
}
private static class EncodingTranslatorHolder {
private static final EncodingTranslator INSTANCE = new EncodingTranslator();
}
public static void main(String[] args) {
EncodingTranslator et = EncodingTranslator.getInstance();
// 인코딩 변환기 프로그램 시작
try {
et.encodingTranslatorExecute(args);
} catch (Exception e) {
e.printStackTrace();
System.exit(101);
}
System.exit(0);
}
/**
* 파일 인코딩 프로그램 실행옵션 분기
* @param args
* @return boolean
*/
public boolean encodingTranslatorExecute(String ...args) throws IOException {
boolean result = false;
Properties prop = new Properties();
InputStream is = Calculator.class.getClassLoader().getResourceAsStream("config.properties");
prop.load(is);
OUTPUT_DIR = prop.getProperty("output.dir.encodingtranslator");
NEW_LINE = prop.getProperty("newline.encodingtranslator");
SEPARATOR = prop.getProperty("separator.encodingtranslator");
REGEX_EXT_FORMAT = prop.getProperty("regex.ext.format.encodingtranslator");
String executeType = "";
String target = "";
String newEncodingType = "";
try {
executeType = args[0];
target = args[1];
newEncodingType = args[2];
} catch (ArrayIndexOutOfBoundsException aoe) {
System.out.println("Somthing Parameter is null");
}
if (isNullOrEmpty(executeType) || isNullOrEmpty(target)) {
System.err.println("Required Parameter is null");
return false;
}
switch (executeType) {
case "-f":
result = encodingTranslatorFileMain(target, newEncodingType);
break;
case "-l":
result = encodingTranslatorFileListMain(target, newEncodingType);
break;
case "-d":
result = encodingTranslatorFilesFromDir(target, newEncodingType);
break;
default:
System.out.println("Inputed parameter $1 invalid. Please Input prameter $1 Ex) something like -f, -l, -d");
}
return result;
}
/**
* 지정한 디렉토리안의 파일들의 인코딩을 일괄 변환한다.
*
* @param dirPath : 변환할 파일들이 들어있는 디렉토리
* @param encoding : 변환할 인코딩
* @return boolean
*/
public boolean encodingTranslatorFilesFromDir(String dirPath, String encoding) {
File [] files;
try {
File dir = new File(dirPath);
FilenameFilter filter = new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(REGEX_EXT_FORMAT);
}
};
files = dir.listFiles(filter);
for (File f : files) {
if(encodingTranslatorFileMain(f.getAbsolutePath(), encoding)) {
System.out.println(f+" translator Success!!");
} else {
System.out.println(f+" translator Failure!!");
}
}
return true;
} catch (Exception e) { e.printStackTrace(); }
return false;
}
/**
* 파일 리스트 (.csv)에 기재된 파일의 인코딩을 변환한다.
* @param filePath : 파일 리스트(.csv) 경로
* .csv[0] : 파일의 절대경로
* .csv[1] : 변환할 인코딩
* @param encoding : 변환할 인코딩
* @return
*/
public boolean encodingTranslatorFileListMain(String filePath, String encoding) {
File fileList = new File(filePath);
if (fileList.isFile()) {
try (BufferedReader br = Files.newBufferedReader(Paths.get(fileList.getAbsolutePath()))) {
String line = "";
while((line = br.readLine()) != null ) {
String [] target = makeArrayFromStr(line);
String newEncoding = "";
try {
newEncoding = target[1];
} catch (Exception e) { newEncoding = null; }
if(encodingTranslatorFileMain(target[0], newEncoding)) {
System.out.println(target[0]+" translator Success!!");
} else {
System.out.println(target[0]+" translator Failure!!");
}
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
return true;
}
return false;
}
/**
* 파일 인코딩 변환프로그램 메인처리
* @param filePath : 대상 파일
* @param encoding : 변환 인코딩
*/
public boolean encodingTranslatorFileMain(String filePath, String encoding) {
String targetFilePath = filePath;
String newEncodingType = encoding;
String nowEncodingType = "";
// null 체크
if (isNullOrEmpty(targetFilePath)) {
System.out.println("Please make sure Input parameter $2. Maybe String inputed is null.");
System.exit(102);
}
// 인코딩이 지정되지 않으면 기본 인코딩으로 UTF-8지정
if (isNullOrEmpty(newEncodingType)) {
System.out.println("Set Default encoding UTF-8");
newEncodingType = "UTF-8";
}
File file = new File(targetFilePath);
try {
// 파일의 현재 인코딩확인
nowEncodingType = getNowEncoding(file);
System.out.println("Encoding Type Currently : ["+nowEncodingType+"]");
// 파일의 인코딩 변환
return encodingTranslatorFile(file, nowEncodingType, newEncodingType);
} catch(IOException ioe) {
ioe.printStackTrace();
System.exit(103);
}
return false;
}
/**
* 파일을 지정한 타입의 인코딩으로 변환하여 출력한다.
* 원본파일은 그대로 보존한다.
* @param f : 변환 대상 파일
* @param nowEncodingType : 파일의 현재 인코딩
* @param newEncodingType : 변환하려는 인코딩
* @return
* @throws IOException
*/
public boolean encodingTranslatorFile(File f, String nowEncodingType, String newEncodingType) throws IOException {
System.out.println("encodingTranslator "+f.getName()+" ["+nowEncodingType+"] -> ["+newEncodingType+"]");
Date date = Calendar.getInstance().getTime();
DateFormat dateFormat = new SimpleDateFormat("yyyymmddhhmmss");
String fileName = f.getName();
int pos = fileName.lastIndexOf(".");
String extension = fileName.substring(pos + 1);
String fileNameWithoutExtension = fileName.substring(0, pos);
String outputFilePath = OUTPUT_DIR+"/"+fileNameWithoutExtension+"_"+newEncodingType+"_"+dateFormat.format(date)+"."+extension;
if (f.isFile()) {
try (BufferedReader br = Files.newBufferedReader(Paths.get(f.getAbsolutePath()), Charset.forName(nowEncodingType))) {
String line = "";
try (BufferedWriter bw = Files.newBufferedWriter(Paths.get(outputFilePath), Charset.forName(newEncodingType),
StandardOpenOption.CREATE_NEW)) {
while((line = br.readLine()) != null ) {
String translatedStr = translatorStr(line, nowEncodingType, newEncodingType);
//System.out.println( " ->"+translatedStr );
bw.write(translatedStr);
bw.write(NEW_LINE);
}
}
return true;
}
}
return false;
}
/**
* 파일의 현재 인코딩을 확인
* @param f
* @return
* @throws IOException
*/
public String getNowEncoding(File f) throws IOException {
String encoding = "";
byte[] buf = new byte[4096];
UniversalDetector detector = null;
if (f.exists()) {
try (FileInputStream is = new FileInputStream(f)) {
detector = new UniversalDetector(null);
int read;
while ( (read = is.read(buf)) > 0 && !detector.isDone() ) {
//System.out.println(read);
detector.handleData(buf, 0, read);
}
detector.dataEnd();
encoding = detector.getDetectedCharset();
return encoding;
}
}
return encoding;
}
/**
* 문자열의 인코딩을 지정한 방식으로 변환한다.
* @param target : 인코딩을 변환하려는 문자열
* @param oldEncodingType : 현재의 인코딩 타입
* @param newEncodingType : 변환할 인코딩 타입
* @return
*/
public String translatorStr(String target, String oldEncodingType, String newEncodingType) {
String originTypeStr = decodeStr(target, oldEncodingType);
return decodeStr(originTypeStr, newEncodingType);
}
/**
* 문자열을 현재 인코딩되어있는 타입으로 디코딩하여 byte배열로 만든 후 byte 배열을 문자열로 변환하여
* 변환된 문자열을 리턴한다.
* @param target
* @param type
* @return
*/
public String decodeStr(String target, String type) {
String decodedStr = null;
try {
decodedStr = new String(target.getBytes(type), type);
} catch (UnsupportedEncodingException e) { e.printStackTrace(); }
return decodedStr;
}
/**
* Null과 공백 확인
* @param str 확인할 문자열
* @return Null 또는 공백이면 true를 리턴한다.
*/
public boolean isNullOrEmpty(String str) {
return (str == null || str.equals(""));
}
/**
* 구분자를 기준으로 입력한 문자열을 자른 문자열의 배열을 리턴하는 메소드
* This method return an Array of String that split String entered based on separator.
* @param targetStr
* @return
*/
public String [] makeArrayFromStr(String targetStr) {
if (isNullOrEmpty(targetStr)) return null;
String [] res = targetStr.split(SEPARATOR);
if (res != null && 0 < res.length) return res;
else return null;
}
}
7. 템플릿
- Java로 파일 인코딩 알아내기
- Java로 파일 인코딩 변환
- Java.nio로 파일 읽고 쓰기
- Java로 디렉터리 안의 파일리스트 가져오기
'프로그래밍 > Java-토이프로젝트' 카테고리의 다른 글
서버 상태 확인 시각화 툴(vmstat,sar와 같은 명령어의 결과 차트화)-1【전체구조와 설계】 (0) | 2021.07.12 |
---|---|
데이터 출력(DB -> json,xml등) 프로그램-3【XML출력】 (1) | 2021.05.23 |
데이터 출력(DB -> json,xml등) 프로그램-2【JSON출력】 (0) | 2021.05.23 |
데이터 출력(DB -> json,xml등) 프로그램-1【전체구조】 (0) | 2021.05.22 |
계산기 (계산식을 입력 받아 처리) (0) | 2021.05.09 |
댓글