Java 대용량 xml 파싱

최근 어떤 작업을 하면서 대용량 xml 데이터를 파싱할 일이 생겼었다. 여지껏 웹 상에서 xml 제공되는 API 와 RSS를 파싱한 적은 많았지만 GB 단위의 대용량 xml 파일은 다루어 본 적이 없었기 때문에 많은 생각을 해보았다. 이러한 데이터를 어떤 방식으로 다루면 좋을까 고민을 하다가 사용한 방법을 이 글을 통해 정리하였다.

인터넷 상에서 검색해보면 대용량 xml 파일을 파싱할 경우에는 SAX 파서를 이용하라는 글이 많았었다. DOM 파서의 경우 전체 문서를 분석하는데 시간이 많이 걸리기 때문에 SAX 파서보다 느리기 때문이다. 하지만 파일로 저장된 GB 또는 TB의 대용량 xml 의 경우에는 SAX 파서를 이용하더라도 많은 문제가 있다. 파서를 이용하기 위해서는 맨 먼저 파일을 String 형식으로 불러와 메모리 상에 로드해 놓아야한다. 하지만 이러한 경우 GB 단위의 데이터만 처리하려해도 메모리가 모자라다. 이렇게 된다면 SAX 파서가 아무리 성능이 뛰어나더라도 문제가 발생한다.

이러한 문제를 해결하기 위해 다양한 방법이 있겠지만, 파일을 스트림을 통해 읽어들이면서 데이터를 처리하는 방법을 생각해보았다. 전체 데이터 값을 한번에 String 으로 만들지 않고, item 별로 String 으로 읽고 이를 DOM 파서를 이용해 처리 한 뒤 MySQL 에 정보를 삽입하였다.


 XML 데이터의 형식

<root>
  <item>
    <node1>value</node1>
    <node2>value</node2>
  </item>
  <item>
    <node1>value</node1>
    <node3>value</node3>
    <node4>value</node4>
  </item>
  ...
</root>

xml 형태의 데이터 양식이다. root 노드의 밑에 item 들이 있고 item 밑에 각각의 정보들이 담겨있다. 이러한 데이터를 가져올 때 item 의 밑에는 각각 다른 노드 이름이 존재 할 수 있기 때문에 이러한 노드에 대해 사전 정보가 없다면 미리 테이블을 구성 할 수 없는 문제가 있다. SAX 파서를 이용한다면 모든 정보에 대해 미리 알고있지 않다면 xml 의 데이터를 가져올 때 누락 될 경우도 발생한다.


Read File

위와 같은 형식의 데이터를 파싱하기 위해 먼저 파일을 String 으로 읽으면서 item 별로 작업을 수행하였다.

BufferedReader in = new BufferedReader(new FileReader("/home/someone/big.xml"));
String line = "", data = "";
while( (line = in.readLine()) != null ) {
  if(line.contains("<item") {
    ArrayList<String[]> item = parseXML(data);
    saveToSQL(item);
    data = line;
  } else {
    data += line;
  }
}

위와 같이 한줄 씩 스트림으로 읽어가면서 “<item” 이라는 글자가 포함되어 있다면 String 값을 해당 라인으로 대입해주고, 포함되어 있지 않다면 기존의 String 값에 새로운 라인을 더해주는 방식으로, 전체 데이터에서 각 item 을 분류한다. 여기서 “<item>” 이 아닌 “<item” 으로 한 이유는 해당 노드에 attribute 들이 존재 할 수 있기 때문이다. attribute 가 존재한다면 > 기호가 다른 라인에서 존재할 경우도 있을 수도 있다. 추가적인 변수를 더 고려한다면 자바 정규식으로 표현하는 것이 더 좋은 방법이다.

이렇게 추출한 각 item 들을 다음 라인을 읽기 전에 DOM 파서를 이용하여 파싱하면 된다.


Parse XML

XML 파싱을 위해 DOM 형식의 html 파서인 Jsoup 라이브러리를 사용하였다. Jsoup 이 아닌 JRE 에 기본적으로 포함되어있는 w3c DOM 라이브러리를 이용하여도 상관없다. xml 파싱을 수행하는 부분은 위의 parseXML(data) 함수 부분이다.

Jsoup Library : http://jsoup.org

public ArrayList<String[]> parseXML(String data) {
  ArrayList<String[]> result = new ArrayList<String[]>();
  Document doc = Jsoup.parse(data);
  Elements items = doc.getElementsByTag("item");
  for(Element item : items) {
    String[] nodes = { item.nodeName(), item.text() };
    result.add(nodes);
  }
  return result;
}

여기서 각 item 의 node 데이터는 HashMap 을 사용하면 편리하긴 하지만 해당 노드의 태그 이름을 모르는 경우에는 HashMap 을 사용하면 더 복잡해지기 때문에, String 배열 형태로 사용하였다. 이러한 과정을 거치면 xml 의 item 의 정보를 얻을 수 있다.

간단히 나타내면
| node1 | value |
| node2 | value |
이러한 형태로 나타 낼 수 있다.


Save to MySQL

위와 같이 구한 데이터를 MySQL 문법을 사용하여 저장한다. 초기에 테이블에는 column 을 추가해 놓지 않고 데이터를 삽입 할 때마다 동적으로 column 을 추가해준다.

public void saveToSQL(ArrayList<String[]> item) {
  DBConnector db = new DBConnector();
  String tags = "", values = "";
  for(int i = 0; i < item.size() ; i++) {
    String tag = item.get(i)[0];
    String value = item.get(i)[1];
    tag = tag.replaceAll("'", "^");
    value = "'" + value.replaceAll("'", "^") + "'";
    db.sendQuery("ALTER TABLE tablename ADD COLUMN " + tag + " TEXT");
    if(i < item.size() - 1) {
      tags += ",";
      values += ",";
    }
  }

  if(tags.length() != 0 && values.length() != 0) {
    String query = "INSERT INTO tablename(" + tags + ") VALUES(" + values + ")";
    db.sendQuery(query);
  }
}

여기서 DBConnector 객체는 JDBC 를 이용하여 만든 db 연결 객체이고 sendQuery 함수는 MySQL 에 명령어를 입력하는 함수이다.

각각의 태그와 값들의 ‘ 문자를 ^ 로 변경한 이유는 ‘ 가 값에 들어가 있을 경우 SQL 문법 오류가 발생하기 때문이다.

 

“Java 대용량 xml 파싱”에 대한 3개의 생각

  1. 블로그 참 깔끔하고, 유용한 정보를 올리는구나~. 나중에 파싱할일 있을 때 참고할께~ 언제 얼굴함보자잉

    1. html xmlns파싱(도와좀주세요)불법파싱 아닙니다^^
      1.https://www.sbobet.com/euro/football 여기에보면 1X2는 승무패를말합니다(여기의 리그,팀명,시간,배당)
      Asian Handicap 이것은 핸디캡 배당을 말합니다(여기의 리그,팀명,시간,배당및 기준값)
      2.https://www.sbobet.com/euro/basketball
      3.https://www.sbobet.com/euro/baseball 여기Money Line
      4.https://www.sbobet.com/euro/ice-hockey
      이것을 실시간으로 파싱하고 싶습니다
      로그인하면 보여지는 페이지가 있습니다(아이디kookoosujin 비번hanhan11551555)
      축구: http://85g1e6h603w6.asia.sbobet.com/web-root/restricted/default.aspx?loginname=754dfae2dec9d7168c641ecf1fa6b554&lang=ko-kr&sportId=1
      한국어로 선택하셔서보셔도되구요…우측에 더보기하면 안에 다른값들이 있는데 그것까지면 더욱좋구요..
      농구: 주소같네요
      — IBCBET.COM —
      http://www.ibcbet.com
      S6KW0100PN3
      asas1212
      축구 : http://d16k8.ibcbet.com/main.aspx Match Odds 1X2=승무패(여기의 리그,팀명,시간,배당)
      http://d16k8.ibcbet.com/main.aspx 같은데 여기 핸디 오버언더 부분들
      농구 야구 모두 주소같습니다
      사실 위에 배당들은 실시간으로 받아지면 좋겠습니다
      그값들이 종종 바뀌거든요..가능하심연락좀^^언어php
      kookoo009@nate.com

댓글 남기기