티스토리 뷰
728x90
반응형
기존에 poi 라이브러리를 이용하여 엑셀 파일 읽기 프로그램을 만들었다.
엑셀 업로드 10만 이상의 대용량 처리 이슈는? 어떻게 하지?
문제점 :
엑셀 사이즈가 커지게 되면 엑셀 파일을 workbook 객체로 변환할 때 Out of Memory(메모리 부족) 현상이 발생하게 된다.
그렇게 되면 엑셀 파일 읽기 도중 서버가 터질 수;; 있는 상황이 발생
그리하여 이보다 좀더 적은 메모리를 이용하여 처리하는 sax 방식을 채택
※ 사용되는 클래스
SheetHandler.java(명칭은 정한것임) -> 엑셀 양식에 맞게 데이터를 커스텀하여 정리하는 클래스
: SheetContentsHandler 를 extends 받아서 override를 하게 된다.
public class SheetHandler implements XSSFSheetXMLMyHandler.SheetContentsHandler{
private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(SheetHandler.class);
//Row 에 해당한다
private List<List<String>> rows = new ArrayList<List<String>>();
//cell 값을 가지게 되는 배열
private List<String> row = new ArrayList<String>();
private List<String> header = new ArrayList<String>();
//cell 위치
private int currRowNum = 0;
//공백 체크
private int emptyCount = 10;
//시작할때 위치를 알려준다.
@Override
public void startRow(int rowNum) {
// int rowNum 은 첫 행을 읽을 때 위치를 말함 ex) 첫 행일 경우 0
this.currRowNum = rowNum;
}
//cell의 끝자락 일 경우 발생하는 함수
//첫 행에 해당하는 컬럼들의 값을 저장
@Override
public void endRow(){
this.rows.add(row);
row.clear();
}
//cell을 하나씩 읽어가는 함수
@Override //String formmatedValue 가 데이터에 해당
public void cell(String cellReference, String formattedValue) {
int thisCol = (new CellReference(cellReference)).getCol(); // cell의 위치에 해당 ex) 첫번째 컬럼일 경우 0번에 해당
if("".equals(formattedValue)){
row.add("");
}else{
row.add(formattedValue);
}
}
@Override
public void headerFooter(String text, boolean isHeader, String tagName) {
// TODO Auto-generated method stub
//sheet의 첫 row 와 마지막 row를 처리하는 메소드
}
}
SAX parsing 클래스 (엑셀 파일을 읽어드리는 함수)
//대용량 데이터 처리사용
public static SheetHandler readLargeExcel(String path,List<PublicCodeVO> publicCodelist) {
SheetHandler sheetHandler = new SheetHandler(publicCodelist);
// 현재 읽고자 하는 파일 가져오기
File file = new File(path);
try {
//OPCPagkage 파일을 읽거나 쓸 수 있는 상태의 컨테이너를 생성
OPCPackage opc = OPCPackage.open(file);
//OPC 컨테이너를 XSSF형식으로 읽어 옴
XSSFReader xssfReader = new XSSFReader(opc);
//엑셀 스타일 형식을 가져오는건데.....
StylesTable styles = xssfReader.getStylesTable();
ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(opc);
//엑셀의 시트를 하나만 가져옴
//만양 여러 시트를 가져와야 할경우 while 문을 통해 처리
//ex) xssfReader.getSheetsData().next(); 대신
// XSSFReader.Sheetlterator itr = (XSSFReader.Sheetlterator)xssfReader.getSheetsData(); -> sheet별로 collection으로 분할함
// while(itr.hasNext()) 를 통해 InputStream inputStream = itr.next(); 를 이용
InputStream inputStream = xssfReader.getSheetsData().next();
InputSource inputSource = new InputSource(inputStream);
//직접적인 sheet의 cell과 row를 생성하는 이벤트
// ContentHandler handle = new XSSFSheetXMLHandler(styles, strings, sheetHandler, false);
//XSSFSheetXMLHandler 를 재정의함
ContentHandler handle = new XSSFSheetXMLMyHandler(styles, strings, sheetHandler, false);
SAXParserFactory saxFactory = SAXParserFactory.newInstance();
SAXParser saxParser = saxFactory.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
xmlReader.setContentHandler(handle);
xmlReader.parse(inputSource);
inputStream.close();
opc.close();
}catch (Exception e) {
LOGGER.error("### excel read file error : {}",e.getMessage());
sheetHandler.setRows(null);
}
return sheetHandler;
}
SAX 파싱 방법을 이용할 때 문제 되는 부분이 있다.
엑셀 양식이 어떤냐에 따라 다르겠지만 만약 한국식 날짜를 이용하는 경우 SAX 파싱을 이용할 경우 강제적 미국식 날짜를 가져오게 된다.
ex) 2021-01-01 -> 01/01/21 로 강제 변환하여 들고 오게됨
이유 : SheetContentsHandler 는 XSSFSheetXMLHandler 에 속하여 있다. 실질적 엑셀에서 데이터를 컨트롤하는 곳은 XSSFSheetXMLHandler이다.
XSSFSheetXMLHandler 는 날짜 변환에 DataFormatter를 이용하고 있다. DataFormatter는 엑셀 데이터에서 날짜가 어떤 locale을 가지던지 미국식 영어로 변환하여 가져오게 된다.
만약 이 사용에 자신이 만드는 프로그램에 연관 없다면 그대로 사용해도 상관 무
하지만 만약 연관이 있다면 이 부분에 대한 커스텀 마이징이 필요
그러므로 재 정의? 개념으로 XSSFSheetXMLHandler를 만들어 DefaultHandler를 상속받는 클래스를 새로 만들어 줘야함.
XSSFSheetXMLMyHandler (내가 이름 정함)
endElement 메소드가 해당 데이터들에 대한 타입을 결정 짓게 되는 부분이다.
해당 부분에 대하여 새롭게 정의하여 사용하면 됨
public class XSSFSheetXMLMyHandler extends DefaultHandler{
...
public void endElement(String uri, String localName, String name)
throws SAXException {
String thisStr = null;
// v => contents of a cell
if (isTextTag(name)) {
vIsOpen = false;
// Process the value contents as required, now we have it all
switch (nextDataType) {
case BOOLEAN:
char first = value.charAt(0);
thisStr = first == '0' ? "FALSE" : "TRUE";
break;
case ERROR:
thisStr = "ERROR:" + value.toString();
break;
case FORMULA:
if(formulasNotResults) {
thisStr = formula.toString();
} else {
String fv = value.toString();
if (this.formatString != null) {
try {
// Try to use the value as a formattable number
double d = Double.parseDouble(fv);
thisStr = formatter.formatRawCellContents(d, this.formatIndex, this.formatString);
} catch(NumberFormatException e) {
// Formula is a String result not a Numeric one
thisStr = fv;
}
} else {
// No formating applied, just do raw value in all cases
thisStr = fv;
}
}
break;
case INLINE_STRING:
// TODO: Can these ever have formatting on them?
XSSFRichTextString rtsi = new XSSFRichTextString(value.toString());
thisStr = rtsi.toString();
break;
case SST_STRING:
String sstIndex = value.toString();
try {
int idx = Integer.parseInt(sstIndex);
XSSFRichTextString rtss = new XSSFRichTextString(sharedStringsTable.getEntryAt(idx));
thisStr = rtss.toString();
}
catch (NumberFormatException ex) {
System.err.println("Failed to parse SST index '" + sstIndex + "': " + ex.toString());
}
break;
case NUMBER:
String n = value.toString();
//formatIndex에 대하여 현재 설정된 번호에 한에서 한국식으로 받아 처리 할 수 있도록 커스터마이징을 진행 대부분 formatIndex 가 14일경우는 날짜로? 가져가는 거 같음
if (this.formatString != null) {
//날짜 형식 관련 데이터들을 locale.Korea 형식으로 변경
if(formatIndex == 14 || formatIndex == 31 || formatIndex == 57 || formatIndex == 58 ||
(176 <= formatIndex && formatIndex <=178) || (182 <= formatIndex && formatIndex <= 196) ||
(210 <= formatIndex && formatIndex <=213) || (208 == formatIndex)) {
sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = DateUtil.getJavaDate(Double.parseDouble(n)); //해당값을 Date형으로 들고오게 함 그리하여 format을 통해 데이터를 재구성함
thisStr = sdf.format(date);
}else {
//Apache POI DataFormatter formatter 는 로케일 형식 무시하고 미국 형식 날짜 표기법으로 반환함
thisStr = formatter.formatRawCellContents(Double.parseDouble(n), this.formatIndex, this.formatString);
}
}else
thisStr = n;
break;
default:
thisStr = "(TODO: Unexpected type: " + nextDataType + ")";
break;
}
// Output
output.cell(cellRef, thisStr);
} else if ("f".equals(name)) {
fIsOpen = false;
} else if ("is".equals(name)) {
isIsOpen = false;
} else if ("row".equals(name)) {
output.endRow();
}
else if("oddHeader".equals(name) || "evenHeader".equals(name) ||
"firstHeader".equals(name)) {
hfIsOpen = false;
output.headerFooter(headerFooter.toString(), true, name);
}
else if("oddFooter".equals(name) || "evenFooter".equals(name) ||
"firstFooter".equals(name)) {
hfIsOpen = false;
output.headerFooter(headerFooter.toString(), false, name);
}
}
...
}
728x90
반응형
'프로그램 언어 > JAVA' 카테고리의 다른 글
java xml 정보 Sax 방식을 이용한 파싱 방법 (0) | 2023.05.02 |
---|---|
클라이언트 실제 접속 IP 가져오는 방법 (0) | 2023.05.02 |
java 엑셀 파일 읽기 기능 part1 (0) | 2023.05.02 |
자바(java) for 문의 종류 (0) | 2023.05.02 |
자바(java) Map 활용하여 list 데이터 비교 처리 방법(효율성 vs 비효율성) (0) | 2023.05.02 |
250x250
반응형
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- Quartz
- 알고리즘
- docker
- Java
- 네이버 클라우드
- dockerfile
- 스케줄러
- 권한
- LocalDate
- 도커
- ncp
- 캘린더
- MySQL
- 캐시
- 격리수준
- 이미지
- 정의
- mybatis
- leatcode
- 개념 이해하기
- 컨테이너
- hazelcast
- 리눅스
- Cache
- Lock
- spring
- dfs
- centos7
- insert
- Linux
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
글 보관함