blah blah

batch rename(python, go)

하위 디렉토리에 있는 파일까지 한번에 파일명 변경해야 되는 일은 자주 겪는 상황임에도 얼마나 정리 습관이 없는지 매번 스크립트 다시 짜곤 하는 것 같다 ㅜㅠ

우선 python 으로 짜보았고

import os
import re

pattern_str = "^test"
rename_str = "about"
ext_str = ".html"
work_dir = r"d:\Test\TEST\mren"

p = re.compile(pattern_str)
for (path, dir, files) in os.walk(work_dir):
    for filename in files:
        ext = os.path.splitext(filename)[-1]
        if ext == ext_str and p.match(filename):
            print("%s\%s => %s" % (path, filename, re.sub(pattern_str, rename_str, filename)))
            os.rename(os.path.join(path, filename), os.path.join(path, re.sub(pattern_str, rename_str, filename)))

공부 겸해서 go 로 엉성하게나마 짜보았다.

package main

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

func renameFiles(path string, f os.FileInfo, err error) error {
    oldNameStr := "^test"
    newNameStr := "about"
    extStr := ".html"

    r, _ := regexp.Compile(oldNameStr)
    if fileName := f.Name(); !f.IsDir() && filepath.Ext(fileName) == extStr {
        if ok, _ := regexp.MatchString(oldNameStr, fileName); ok {
            r1 := regexp.MustCompile(fileName)
            newPath := r1.ReplaceAllString(path, r.ReplaceAllString(fileName, newNameStr))
            fmt.Println(newPath)
            err := os.Rename(path, newPath)

            if err != nil {
                fmt.Println(err)
                return err
            }

        }
    }

    return nil
}

func main() {
    flag.Parse()
    filepath.Walk(flag.Arg(0), renameFiles)
}

두 언어 다 기본 문법 겨우 아는 수준이라 뭔가 냄새가 나는데 어떻게 손댈지 엄두가 안나는 …

blah blah

yaml 을 spring 에서 사용하기

스프링 프레임워크 쪽에서는 공백 문자로 설정 분류하는 것이 모호하다며 지원할 계획이 없는듯, 하지만 spring-boot 에서는 yaml 포맷의 설정 파일을 이용하는 듯 해서 여기에서 구현해놓은걸 그대로 가져다 써보기로 했다.

참조 소스 : https://github.com/cloudfoundry/uaa/tree/master/common/src/main/java/org/cloudfoundry/identity/uaa/config

test1.yml

bill-to: &id001
   given : Chris
   family : Dumars
   address:
       city : Royal Oak
       state : MI
       postal : 48046
ship-to: *id001
product:
   - sku :
        item1: abc
        item2: efg
     quantity : 4
     description : Basketball
   - sku : BL4438H
     quantity : 1
     description : Super Hoop
 taxyn : false
 tax : 251.42
 total: 4443.52

applicationContext.xml

<bean id="ymlFactoryBean"
      class="org.cloudfoundry.identity.uaa.confi.YamlPropertiesFactoryBean">
   <property name="resources">
      <list>
         <value>classpath:test1.yml</value>
         <value>...</value>
      </list>
   </property>
 </bean>

테스트

ApplicationContext context = new ClassPathXmlApplicationContext(“applicationContext.xml”);

Properties yml = (Properties) context.getBean(“ymlFactoryBean”);
System.out.println(“# date : ” + yml.get(“date”));
System.out.println(“# bill-to.given : ” + yml.getProperty(“bill-to.given”));
System.out.println(“# product[0].sku.item1 : ” + yml.getProperty(“product[0].sku.item1”));
System.out.println(“# product[1].sku : ” + yml.getProperty(“product[1].sku”));
System.out.println(“# ship-to.give : ” + yml.getProperty(“ship-to.given”));

boolean taxYn = (Boolean) yml.get(“taxyn”);
if(taxYn) {
     System.out.println(“# tax : ” + yml.get(“tax”));
}

아직 전체적인 구조 이해를 못해서 그리 깔끔한 방법은 아니지만 일단 …

blah blah

UUID ByteArray

Java 에서 UUID 값을 바이트 배열로 변환. 바이트 배열값을 UUID 로 변환하는 기능을 stackoverflow 에 올라온 여러가지 글들을 참고해서 간단히 짜보았습니다.

 

import java.nio.ByteBuffer;
import java.util.UUID;

import com.google.common.primitives.Longs;

 

public class Uuids {
    public static byte[] toByteArray(UUID uuid) {
        ByteBuffer buff = ByteBuffer.wrap(new byte[16]);
        buff.putLong(uuid.getMostSignificantBits());
        buff.putLong(uuid.getLeastSignificantBits());

        return buff.array();
}

public static UUID fromByteArray(byte[] byteArray) {

        long mostSigBits = Longs.fromByteArray(byteArray);
        long leastSigBits = 0;
        for (int i = 8; i < 16; i++)
            leastSigBits = (leastSigBits << 8) | (byteArray[i] & 0xff);

 

        return new UUID(mostSigBits, leastSigBits);
}

 

테스트

@Test
public void testFromByteArray() {
    UUID u = UUID.randomUUID();

    byte[] bt = Uuids.toByteArray(u);

    assertEquals(u, Uuids.fromByteArray(bt));
}

blah blah

Scala – $tag method

참고 : 스칼라의 Class

1. age_$eq(int)

위 글에 설명되어 있듯 age_=()는 setter입니다.

즉, 실제 사용 시

outsider.age = 30

이라고 할 때 이것은 아래와 같이 호출하고 있다고 할 수 있습니다.

outsider.age_=(30)

그런데 왜 age_= 가 아니고 age_$eq 로 바뀌었냐면 JVM 에서는 ‘=’ 기호를 메서드명에 사용하는 것이 허용되지 않기 때문입니다.

2. $tag()

public int $tag() throws java.rmi.RemoteException;

뭐 하는 놈일까? … 이렇다는군요.

This(=$tag()) is a method defined within the ScalaObject trait designed to help optimize pattern matching. Unfortunately, it also means yet another abstract method which must be defined when implementing Scala traits which contain method definitions. ( via Interop Between Java and Scala )

blah blah

gpath 로 값 뽑을 때 포함된 tag 를 보존하려면

gpath 의 간편함에 감탄한지 채 얼마 안되어 예상치않은 문제에 직면.


<contenttitle>End of Grace Period</contenttitle>
<viewdate>2011-06-24 08</viewdate>
<contents>
<contentimg/>
<content>
<p><b>tag test</b></p>
</content>
</contents>

    def xml = new XmlSlurper().parse(xmlFile)
    assert “<p><b>tag test</b></p>” == xml.contents.content.text()
   

어찌나 간편하고 깔끔하던지 값에 포함된 태그들을 알아서 다 날려버리더군요.

결국 이번 같은 경우나 CDATA  값을 그대로 가져오려는 경우 gpath 사용 시 문제가 있네요.

XmlSlurper 대신 XmlParser 를 이용해도 쉽게 해결할 수 있을 것 같지 않고 결국 Xml 파서를 JDOM 이나 이런 것으로 바꿔야하나 갈등하다가 약간 꽁수를 써봤습니다.

    def xml = new XmlSlurper().parse(xmlFile)
    content = XmlUtil.serialize(xml.contents.content)
    def matches = content =~ /(?s)<content>(.*?)<\/content>/
    matches.each { assert “<p><b>tag test</b></p>” == it[1] }

핵심은 GPathResult 를 groovy.xml.XmlUtil.serialize 를 이용해서 XML string 으로 재변환한 뒤 정규식으로 값을 재추출하는거.

p.s. 기본 아이디어는 ‘Convert GPathResult to String without tag0‘에서 구했습니다.

blah blah

groovy CharsetToolkit 보충

groovy CharsetToolkit 으로 구현했던 스크립트에 약간 문제가 있어 그거 보충하느라 …

******

1. groovy CharsetToolkit

얼마 전 groovy의 CharsetToolkit 을 이용한 방법을 소개해드렸는데 이걸로 테스트해보던 어떤 분이 문제점을 하나 발견하고 원인까지 찾아내주셨습니다.

http://groovy.codehaus.org/api/groovy/util/CharsetToolkit.html

Uicode files encoded in UTF-16 (low or big endian) or UTF-8 files with a Byte Order Marker are correctly discovered. For UTF-8 files with no BOM, if the buffer is wide enough, the charset should also be discovered.

A byte buffer of 4KB is used to be able to guess the encoding.

이 문서에 언급된 저 설명에 비추어보면 4KB 이내에 파일 charset 을 정확히 판단할 수 있는 문자가 포함되어 있지 않은 경우는 제대로 추정 못할 수 있을 수 있음을 알 수 있습니다.

그 분이 발견해서 지적해 준 문제가 이에 해당하는 거였는데

  • 파일 앞 부분에 영문만 있다가 나중에야 한글 문자가 포함되어 있는 어떤 파일에서 charset 을 제대로 찾아내지 못함.
  • 파일 내용 앞 부분에 한글 문자를 임의로 하나 삽입해보니 제대로 charset 감지함.

문제 피드백 받고 좀 더 찾아본 과정에서 몇가지 알게된 점을 간단히 정리해봅니다.

결국은 용량이 4K 가 넘지 않는 텍스트 파일에 대해서는 groovy 의 CharsetToolkit 을 쓰면 쉽게 처리 가능하지만 그렇지 않은 경우에 대해서는 좀 더 고민이 필요할 것 같습니다.

2. java 의 encoding detector

이왕 이렇게 된거 조금 더 진행해보기록 했습니다.

java에서 charset 을 알아내는 것을 편하게 해주는 오픈소스 라이브러리로 몇 가지를 찾을 수 있었습니다.

언급된 라이브러리 중에서 juniversalchardet을 이용해서 간단히 테스트를 해보았습니다.

import groovy.io.FileType
import org.mozilla.universalchardet.UniversalDetector

folder = “E:\\encoding_test\\data”

def guessEncoding(byte[] bytes) {
        def DEFAULT_ENCODING = “UTF-8”
        detector = new UniversalDetector(null)
        detector.handleData(bytes, 0, bytes.length)
        detector.dataEnd()
        encoding = detector.getDetectedCharset()
        detector.reset()

        if (encoding == null) {
              encoding = DEFAULT_ENCODING
        }

         return encoding
}

def detectCharset() {
        new File(folder).traverse(type:FileType.FILES, nameFilter:~/.*\.java|.*\.xml/) {
                charSet = guessEncoding(it.getBytes())
                 println “$it.name : $charSet”
        }
}

detectCharset()

지난 번 짰던 소스를 그대로 이용하느라 groovy 에서 테스트해보긴 했으나 guessEncoding 메서드 부분에서 키워드 몇 개만 바꾸면 당연히 자바에서 그대로 사용 가능합니다.

테스트해본 바로는 위 1번에서 문제가 되었던 파일도 제대로 charset 을 찾아내네요.

다양한 charset 형태 파일로 테스트해보진 않았는데 관심있는 분들 있다면 한 번 테스트해보고 피드백 부탁드립니다.

그리고 universalchardet, jchardet, cpdetector, ICU4J  외에 괜찮은 다른 오픈 소스 추천해주셔도 고맙구요.

3. groovy 스크립트를 자바에서 사용하는 방법

*** GroovyCharSetTest.groovy

package groovy;

import groovy.io.FileType
import groovy.util.CharsetToolkit

public class GroovyCharSetTest {
         def getCharSet() {
               def folder = “D:\\charSetTest”

               def charSet = [:]

               new File(folder).traverse(type:FileType.FILES) {
                       charSet[it.name] = new CharsetToolkit(it).getCharset().toString()
               }

               return charSet
        }
}

 *** TestCharSet.java

package groovy;

import java.util.Iterator;
import java.util.Map;

public class TestCharSet {
        public static void main(String args[]) {
                GroovyCharSetTest gcst = new GroovyCharSetTest();

                Map map = (Map)gcst.getCharSet();
                Iterator iterator = map.keySet().iterator();
                while (iterator.hasNext()) {
                        String key = (String) iterator.next();
                        System.out.println(key + ” : ” + map.get(key));
                }
        }
}

지난 번 글에 올렸던 샘플을 자바에서 호출해서 사용해보려고 하신 분이 있어서 간단히 샘플을 만들어 보았습니다.
그 샘플을 직접 이용하는 경우 약간의 문제가 있어서 그에 대한 부분 (  def 키워드 관련 )에 대해서 답변 메일로 보냈던 내용을 그대로 옮겨 봅니다.
  1. No such property: folder for class: groovy.GroovyCharSetTest
    • groovy 에는 def 라는 변수나 함수 선언 시 사용하는 타입명을 대체해주는 키워드가 있는데 이걸 붙여주지 않아도 보통 때(?)는 상관 없습니다.
      • 이렇게 구문의 편리성을 제공하는 걸 Syntatic sugar 라고들 합니다. 특히 ruby, python 등의 스크립트 언어들에서 Syntatic sugar 가 많이 제공됩니다.
      • 일반 스크립트 구문에서는 def 붙이는 것과 그렇지 않은 것이 차이 없으나 변수를 class 안에서 사용할 때는 달라집니다.
    • 이건 groovy 에서의 closure 등과 관련되는 문제인데 … 일단 여기서는 간단하게 변수의 통용 범위 문제라고 이해하셔도 될 것 같네요.
    • 핵심은 이번 처럼 Class 안에서 사용하는 변수에는 명시적으로 def 를 붙여주셔야 합니다.
  2. 자바에서 소스 수정한 부분
    • groovy에서 제공하는 Map 이라는 타입( def charSet = [:] 부분 ) 으로 처리값 리턴하게 해주고 java 에서는 이걸 역시 java.util.Map 으로 받아 처리해주도록 했습니다.

추가로  groovy 로 만든 프로그램을 일반 자바 프로젝트에서 사용하는 방법은 몇 가지가 있는데 그 중 간단한 방법은 다음과 같습니다.

  1. groovy 설치 후 bin 디렉토리에 보면 groovyc 라는게 있습니다. groovyc 이용해서 그루비 스크립트를 자바 클래스 파일로 컴파일.
    • 가령 groovyc GroovyCharSetTest.groovy 와 같이 해주면 패키지명에 맞추어 알아서 클래스 파일 생성해줍니다.
  1. groovy 설치 디렉토리 아래 embeddable 에 있는 groovy-all-***.jar 파일을 자바 프로젝트의 클래스패스에 추가해주면 1에서 컴파일한 클래스 파일을 사용 가능합니다.

p.s :  충분한 예는  아니나 groovy 에서 java 라이브러리를 이용하는 방법은 2번을, java 에서 groovy 스크립트 이용하는 방법은 3번을 참고해볼 수 있을겁니다.

blah blah

groovy – CharsetToolkit 등등

이번에도 groovy.

간단한 작업 위해 만든 스크립트인데 나중에 써먹을 만한게 몇가지 있어서 적어봅니다.

작업 개요 : 특정 폴더 하위에 있는 모든 폴더의 자바 파일의 인코딩을 MS949 에서 UTF-8 로 전환

간단하니 일단 소스 부터.

import groovy.io.FileType
import groovy.util.CharsetToolkit

folder = "E:\\encoding_test\\data"
result = "E:\\encoding_test\\result"

new File(folder).eachDirRecurse {                            // (1)
    subDir = it.path.replace(folder, "")
    new File("${result}\\${subDir}").mkdirs()           // (2)
}

new File(folder).traverse(type:FileType.FILES, nameFilter:~/.*\.java/) {
    charSet = new CharsetToolkit(it).getCharset();          // (3)
    target = result + it.path.replace(folder, "")

    if(charSet.toString() != "UTF-8") {
        new FileOutputStream(target).withWriter('UTF-8') { writer ->        // (4)
            new FileInputStream(it).withReader('MS949') { reader ->
                writer << reader
            }
        }
    }
    else {
        new AntBuilder().copy (file : it , tofile : target)                           // (5)
    }
}

(1)의 eachDirRecurse() 는 recursive 하게 디렉토리 정보를 구하는 메서드입니다.

( 제 컴 환경에서는 그냥 eachDir() 만으로도 되던데 딴 분 컴에서는 제일 상위의 서브 디렉토리만 찾는 문제가 있더군요. OS 차이 때문일까요? )

(2)에서는 mkdirs() 를 이용해서 결과 파일 저장할 디렉토리 하위에 작업 대상 디렉토리 구조와 동일하게 서브 디렉토리를 만들어 주었습니다.

(3)이 이번 작업하면서 알게된 꽤 유용한 groovy 에서 지원되는 기능인데 … groovy.util.CharsetToolkit 라는 캐릭터셋 처리 위한 패키지가 있더군요.

CharsetToolkit 에서 제공되는 getCharset()이란 메서드를 이용해서 파일의 캐릭터셋을 쉽게 알아낼 수 있습니다.

이걸 쓴 이유는 작업하려는 파일들에 MS949, UTF-8 파일이 섞여 있어서 무조건 인코딩을 바꿔주면 원래 UTF-8 형식이었던 파일은 한글이 깨지는 문제가 발생하더군요.

그래서 처리 전 파일의 캐릭터셋을 미리 확인해서 UTF-8인 경우에는 그냥 인코딩 변환 없이 결과 디렉토리로 복사만 해주었습니다.

(4) 이하 몇 줄은 ‘MS949’ 형식 파일을 UTF-8로 변환해주는거구요.

(5)번은 파일 복사하는 부분입니다.

자바와 마찬가지로 File 클래스에 보면 renameTo()  라는 메서드가 있어서 rename 이나 move 등의 용도로 쓸 수 있습니다. (renameTo 쓰는 예는 지난 번 글에 있으니 참고하세요)

하지만 copy 를 위한 메서드는 따로 있지 않으므로 직접 구현해야 하는데 대신 AntBuilder의 copy 기능을 이용하면 쉽게 처리할 수 있습니다.

( AntBuilder는 별도로 import 처리할 필요도 없습니다 )

이게 전부입니다. 딴거야 자바로 처리해도 크게 아쉬운거 없지만 groovy.util.CharsetToolkit 은 알아두면 꽤 유용하게 쓸 수 있을 듯 싶네요.

blah blah

groovy 사용 간단 예 – gpath, withBatch 등

이번에는 groovy.

******

프로젝트 지원 나와서 간단한 작업 하나를 받았습니다.
특정 디렉토리 안에 있는 동일한 구조의 xml 파일들에서 몇 개의 element 값을 추출해서 DB 에 넣는거랍니다.

일단 뭘로 짤까 잠시 고민했는데 텍스트 파일 처리하는 이 정도의 작업이면 역시나 스크립트 언어가 적격이라 판단했습니다.
그리고 이 작업 맡을 분이 자바 사용자니 딴거보다는 적응이 용이하겠다 싶어서 스크립트 언어 중에서 groovy 를 선택했습니다.

그럼 본격적으로 …

xml 은 대충 이런 식.

<?xml version=”1.0″ encoding=”UTF-8″?>
<abouthjs>
<titleimg>../img/news/tit_notice.gif</titleimg>
<contenttitle>어쩌구 저쩌구 24HR FAQ)</contenttitle>
<appendimg/> <viewdate>2010-11-10 13</viewdate>
<contents>
<contentimg/>
<content>어쩌구 저저꾸 24HR rule 관련 Customer Advisory…</content>
</contents>
<servicekind>T</servicekind>
<localekind/>

</abouthjs>

여기서 contenttitle, content, viewdate 등의 값을 추출해야 합니다.

groovy 소스부터 보겠습니다.

import groovy.io.FileType
import groovy.sql.*

def getXmlElement(xmlFile) {
def xml = new XmlSlurper().parse((xmlFile.path).replace(“\\”, “/”))   // (3)
def eleList = []
eleList.add(xml.contenttitle.text())                                  // (4)
eleList.add(xml.contents.content.text())
eleList.add(xml.viewdate.text())
return eleList
}

def insertDb(resultMap) {
def db = Sql.newInstance(“jdbc:oracle:thin:@dbUrl:port:dbSid”, “id”, “pw”, “oracle.jdbc.driver.OracleDriver”) // (5)
db.withBatch(100, “INSERT INTO test (val1, val2, val3) VALUES (?, ?, ?)”) { ps ->                               // (6)
resultMap.each { k, v ->
ps.addBatch(v)                                                // (6-1)
}
}

db.close()
}

def folder = args?.size() ? args[0] : “.”                            // (1)
resultMap = [:]
new File(folder).traverse(type:FileType.FILES, nameFilter:~/.*\.xml/, maxDepth:0) {    // (2)
resultMap[it.getName()] = getXmlElement(it)                                                         // (2-1)
}

insertDb(resultMap)

단순한 작업이라 달랑 이게 전부입니다.

groovy 의 아주 기초 문법들은 일단 패스하고 이 작업에 사용한 groovy 의 몇가지 기능만 추려보겠습니다.

  • job1. 디렉토리에서 파일 목록 구하기 : File
  • job2. XML 에서 데이타 추출 : gpath
  • job3. DB 처리 : groovy sql

소스 실행 방법은

C:\>groovy XmlTest.groovy C:\data\xml

( 소스 파일명은 XmlTest.groovy, XML 파일이 있는 디렉토리는 C:\data\xml 이라고 했습니다. )

소스 실행하면 진입점은 (1)이 됩니다.
인자로 준 디렉토리를, 인자가 없을 경우에는 현재 디렉토리를  변수로 받습니다.

job1에 해당하는게 (2)입니다.
지정한 디렉토리에서 파일 목록 구하는 방법은 꽤 많은데 이번 경우에는 traverse(…)라는 함수를 사용해봤습니다.
xml 파일 목록만 구하기 위해 nameFilter:~/.*\.xml/ 를, 검색할 디렉토리의 하위 디렉토리는 찾을 필요 없기에 maxDepth:0 등의 패러미터를 주었습니다.
(2-1)에서는 루프 돌면서 각각의 파일 정보(it)를 넘겨서 원하는 element 값들을 되돌려받아 resultMap 이라는 Map 에 파일명을 키값으로 담습니다.

job2에 관련된게 getXmlElement(…)입니다.
(3)의 XmlSlurper().parse 에서 xml 파일 읽어들였고 (4)에서 원하는 element 값들 추출합니다.

(4)에서 쓰인게 gpath 란 기능인데 이건 xpath 의 groovy 구현체라고 생각하면 됩니다.
xml 샘플의 구조와 xml.contents.content.text() 를 매칭시켜보면 gpath 가 얼마나 간편하게 특정 노드의 데이타를 가져올 수 있는지 알 수 있습니다.
xpath 란거 이름만 들어봤지 실제 써보지 않았는데 xml 데이타 추출하는데서 만큼은 DOM이나 SAX 쓰는 것보다 무지 편한 것 같습니다.
자바5 부터는 javax.xml.xpath 패키지가 기본 포함되었다 하니 앞으로 자바 코딩할 일 있을 때면 xpath 를 애용하게 될 것 같네요.

job3에 해당하는건 insertDb(…)입니다.
groovy 역시 sql 사용 위해서는 jdbc 쓰므로 자바 코딩할 때 처럼 (5)번 써주면 되고 기타 groovy sql 기본 사용법은 패스.
이번 코딩에서는 groovy 1.8.1 버전 부터 지원되기 시작한 자바의 batchUpdate 에 해당하는 withBatch를 써보았습니다.

자바의 batchUpdate는 아시다시피 다수의 sql문을 배열에 담은 후 한꺼번에 처리해주어 성능상의 이점을 얻을 수 있죠.
마찬가지로 (6-1) addBatch 를 통해 복수의 xml 파일에서 추출한 값들을 순차적으로 넣어줍니다.

(6)의 insert 문 보면 필요한 bind 변수가 3개 인데 (6-1) ps.addBatch(v) 한 줄로 처리되었나 궁금하신 분도 계실겁니다.
여기서 v 는 소스 따라가보면 getXmlElement 에서 리턴한 eleList 라는 List 타입의 변수값입니다.
즉, v 에는 [“Test page”, “테스트 컨텐츠”, “2011-11-10 13”] 과 같은 형태로 값이 들어가 있는 셈이므로 저 한 줄 깔끔하게 처리됩니다.

batchUpdate 안쓰고 일반적인 방식으로 하자면 다음 정도로 하면 될겁니다.

def db = Sql.newInstance(…)
new File(folder).traverse(
type:FileType.FILES,
nameFilter:~/.*\.xml/,
maxDepth:0
) {
resultList = getXmlElement(it)
db.execute “INSERT INTO test (val1, val2, val3) VALUES (?, ?, ?)”, resultList
};db.close()

이상입니다.

*****

위 내용은 프로토타입으로 작성한거고 실제 구현했던거는 제약 조건이 꽤 많아서 소스 전면 수정( 한 300백 라인 정도)했습니다.

이 때 추가된 걸로 몇가지 간단하지만 나중에 재활용할만한거 두 어가지만 추가 정리

def getDurationTime() {
def timeStart = new Date()

….

def timeStop = new Date()
TimeDuration duration = TimeCategory.minus(timeStop, timeStart)

log.info duration
}

첨부 파일 처리할 때 쓴거로 유니크한 파일명 부여 위해서 uuid 사용한거

def getUuidFileName(fExt) {
uuid32 = getUUID()
if (!fExt.isAllWhitespace() && fExt.size() < 4) {
uuid32 = uuid32.substring(0, uuid32.size() – fExt.size() – 1) + “.” + fExt
}

return uuid32
}

 

 

 

blah blah

java properties 추출 위한 엑셀 매크로

이번에는 excel macro. 급조한거라 함수명이나 로직이 조악하기는 하지만 …

**********

자바 웹 개발 시 웹상의 다국어 페이지 관리 등을 위해 ResourceBundle 방식을 이용해서 메시지나 레이블 등을 properties 파일에 넣어 활용하곤 합니다.

그런데 보통 실제 웹 디플로이 전에는 관리를 위해 이 컨텐츠들을 엑셀로 취합 관리하는 경우가 일반적이고 변경 시에는 여기 내용을 복사해서

  • 텍스트 파일에 붙여 넣은 후 한글 등의 경우에는 native2ascii 를 실행해서 변환해주거나
  • 이클립스 사용자들은 이 추가 작업이 불편해서 PropetiesEditor 등을 사용하는 듯 합니다.

(옛날에는 보통 이런 식으로 했었지만 아마 요즘은 진일보한 방법 쓰고 있겠죠? 방법 공유 좀 해주세요 )

오늘은 엑셀에서 관리하는 내용을 properties 파일로 자동 추출하기 위해 짜본 간단한 매크로를 공유 차원에서 적어봅니다.

( 물론 POI 나 JExcelApi 등 이용해서 자바로도 충분히 가능한 작업입니다만 이 작업을 수행할 담당자가 개발자가 아니란 점을 고려하면 … )

  A B C D
1        
2   메시지 관리    
3 일련번호 영문 국문 중문
4 EST00001 Detail Schedule 상세 스케줄 详细船期
5 EST00002 Route Information 라우트 정보 路径信息

가령 엑셀의 문서 형식이 위와 같다고 할 때 매크로 소스는 아래와 같습니다.

소스의 VBA 문법에 관해서 설명은 건너뛰겠습니다.

데이타 추출해서 텍스트 파일로 저장 시 유니코드 값으로 전환하는 기능은 get_codepoints(…)에 구현해 놓았습니다. 여기서 핵심은 Hex(AscW(Mid$(msg, i, 1))) 입니다.

이 매크로 그런대로 잘 잘동했습니다만 중문의 간체의 유니코드를 처리 못하는 문제가 있더군요.

위 엑셀 샘플에서 详细船期 란 데이타 중 ‘详细’가 간자체입니다. 해당 글자의 번자는 ‘詳細’라고 하던데 아무튼 위에 언급한 Hex(…) 구문만으로는 ‘详细’의 정확한 값을 못가져옵니다.

그래서 간체 문제는 중문의 LocaleID (LCID) 값을 지정해주는 것으로 일단 해결했습니다 .

txt = txt & StrConv(Hex(Asc(Mid$(msg, i, 1))), vbFromUnicode, 1028) 이런 식으로 하면 되네요.

대충 기본적인 문제들 해결한 VAB 매크로 소스는 다음과 같습니다.

Sub ExportText()
    Dim fso As Object
    Dim dict As Dictionary, dict1 As Dictionary
    Dim v As Variant
    Dim sPath As String
    Dim key_col As String

    title_str = GetTitle()
  
    Set dict = New Dictionary
    With dict
       .Add “en”, title_str
       .Add “ko”, title_str
       .Add “zh”, title_str
    End With
   
    Set dict1 = New Dictionary
    With dict1
       .Add “ko”, Array(4, 4)
       .Add “en”, Array(5, 5)
       .Add “zh”, Array(6, 6)
    End With

    es_pos = pFindRowPos(“ESW”)
    es_title = vbCrLf & “#ES(e-Service)” & vbCrLf
   
    last_row = GetLastRow(“Sheet1”, “A”)
    For i = 3 To last_row
        key_col = Cells(i, 1)
        If i = es_pos Then
            For Each v In dict.keys
                dict(v) = dict(v) & es_title
            Next
        End If
       
        If key_col <> “” Then
            For Each v In dict.keys
                dict(v) = GetPropertieString(dict(v), key_col, Cells(i, dict1(v)(0)), v)
            Next
        End If
    Next i
   
    sPath = ActiveWorkbook.Path

    Workbooks.Open sPath & “\contents_v1.0.xlsx”
        find_pos = pFindRowPos(“WS”)
        last_row = GetLastRow(“Sheet1”, “A”)
       
        For i = find_pos To last_row
            key_col = Cells(i, 1)
            If key_col <> “” Then
                For Each v In dict.keys
                    dict(v) = GetPropertieString(dict(v), key_col, Cells(i, dict1(v)(1)), v)
                Next
            End If
        Next i
    ActiveWorkbook.Close

    Set fso = CreateObject(“scripting.filesystemobject”)
    For Each v In dict.keys
        r = CreateFile(fso, sPath, v, dict.Item(v))
    Next
    Set fso = Nothing
   
    MsgBox “done”
End Sub

Function GetTitle()
    GetTitle = “##############################” & vbCrLf _
             & “# Message Properties” & vbCrLf
             & “# 1. AM(Admin)” & vbCrLf _
             & “#################################” & vbCrLf _
             & “#CM(Common)” & vbCrLf
End Function

Private Function pFindRowPos(stext As Variant, _
  Optional searchdirection As XlSearchDirection = xlNext, _
  Optional searchorder As XlSearchOrder = xlByRows) As Long

    Dim lresult As Long, org As Range

    Set org = Cells.Find(what:=stext, LookIn:=xlValues, _
    lookat:=xlPart, searchorder:=searchorder, _
                 searchdirection:=searchdirection, _
    MatchCase:=False, searchformat:=False)

    If Not org Is Nothing Then lresult = org.Row

    pFindRowPos = lresult

    Set org = Nothing

End Function

Function GetLastRow(data_sheet As String, key_column As String)
    Worksheets(data_sheet).Activate
    With ActiveSheet
        LastRow = .Cells(.Rows.Count, key_column).End(xlUp).Row
    End With
   
    GetLastRow = LastRow
End Function

Function GetPropertieString(curr_txt As String, key_col As String, cel_val As String, lang As Variant)
    Dim result As String
    result = curr_txt & key_col & ” = ” & GetCodePoints(cel_val, lang) & vbCrLf
   
    GetPropertieString = result
End Function

Private Function CreateFile(fso As Object, sPath As String, prop_file As Variant, msg_str As String)
    Set lang_prop = fso.CreateTextFile(sPath & “\message_” & prop_file & “.properties”, True)
    With lang_prop
        .Write msg_str
        .Close
    End With
End Function

Function GetCodePoints(msg As String, lang As Variant)
    Dim txt As String
    Dim iChar As Integer
   
    msg = Replace(msg, Chr(10), ” “)
    For i = 1 To Len(msg)

        iChar = AscW(Mid$(msg, i, 1))

        If iChar = 63 Then
            If lang = “zh” Then
                If Mid$(msg, i, 1) = “?” Then
                    txt = txt & “?”
                Else
                    txt = txt & StrConv(Hex(Asc(Mid$(msg, i, 1))), vbFromUnicode, 1028)  ‘간체
                End If
            Else
                txt = txt & “?”
            End If
        ElseIf iChar = 160 Then              ‘공백 특수문자 처리
            txt = txt & ” ”
        ElseIf (iChar < 128 And iChar > 0) Then ‘Or (iChar > 8210 And iChar < 8304) Then   ‘“ ” ‘ ’ 처리
            txt = txt & Mid$(msg, i, 1)
        Else
            uni = StrConv(Hex(AscW(Mid$(msg, i, 1))), vbLowerCase)
            If Len(uni) = 2 Then
                uni = “00” & uni             ‘ub7, ub0, ue1
            End If
            txt = txt & “\u” & uni
        End If
    Next i

    GetCodePoints = txt
End Function

  1. Dictionary 란걸 사용했는데 이걸 쓰려면 scrrun.dll 을 사용할 수 있도록 엑셀의 ‘도구 > 참조’에서 Microsoft Scripting Runtime’ 을 선택해주어야만 합니다. 
  2. pFindRowPos 라고 특정 문자열이 들어있는 셀의 row 위치 구하는 함수는 http://excelvbamacro.com/how-to-find-row-position-of-a-particular-text.html 에서 구했습니다.

 덤으로 이 기능을 ruby 로 간단하게 구현한 테스트 코드도 적어 봅니다.

require ‘win32ole’
require ‘active_support’
 
#excel = WIN32OLE::new(‘excel.Application’)
#sheet = excel.Workbooks.Open(‘E:\test1.xlsx’).Worksheets(‘Sheet3’)
 
excel = WIN32OLE::connect(‘excel.Application’)
sheet = excel.activesheet
sheet.Range(‘B4:E200’).columns.each { |col| col.cells.each { |cell| puts cell.Value + ” : ” + ActiveSupport::JSON.encode(cell.Value) } }
 
루비 소스는 제대로 쓰려면 한참 다듬어야 하지만 그래도 기본 기능은 들어가 있으니 … ( ActiveSupport::JSON.encode(cell.Value)가 unicode 의 code print 값 구하는 부분입니다.)
아직 간자 문제 등은 처리하지 않은 상태인데 엑셀에서 해결한 방식처럼 encode 함수 사용 시에 Locale 코드값을 설정하는 방법이 있는지 좀 찾아봐야겠습니다.