자바에 대한 공부를 시작하다.

Delphi .Net의 단종으로 인해 앞으로 검색서비스를 자바를 이용해 구축해 볼까 하고
자바를 공부하기 시작했다. 루씬과 관련된 다른 프로젝트(slor,hadoop)도 자바로 되어 있어서
자바를 한번 공부해 볼 필요성은 느끼고 있던 차였다.
90년대 자바의 초창기 때 자바로 데스크탑 어플리케이션을 만들어 볼까하고 잠시 자바를 들여다
본 이후 처음으로 자바를 다시 보게 되었다.
[Head First Java]라는 책이 편집 스타일이 아주 참신해서 구입했는데 나하고는 안맞는 것 같다.
내게는 기술 문서 매뉴얼 스타일이 더 어울리는 듯 :)
자바랑 C#이랑 비슷한 점이 많아서 배우는 데 시간은 많이 절약될 것 같다.

by 미노 | 2009/11/04 12:25 | 트랙백 | 덧글(0)

아~~ Delphi .Net 단종...

델파이 2009부터 Delphi .Net를 지원하지 않는다는 걸 얼마전에야 알았다.
그동안 RAD2006의 Delphi .Net를 이용해 색인과  검색 기능을 만들었는데
이제 어쩌란 말이냐...
Delphi .Net이 지원되는 마지막 버전이 2007이니 2007버전을 사용하든가 아니면
최신 버전에서 지원되는 Delphi Prism를 사용하는 두방법이 있지만 두 방법 모두
좋은 방법은 아닌 것 같다. Delphi Prism은 델파이와는 다른 언어이고 잎으로의 미래도
불투명하기 때문에 Delphi .Net의 대안은 되기 어려울 것 같다.

아마도 이제는 델파이을 버리고 자바를 해야할 것 같다.
루씬이 원래 자바로 되어 있고 관련 프로젝트도 자바이기 때문에 자바로 하면 여러 잇점이
있다. 그리고 서버도 윈도우즈를 사용하지 않고 리눅스를 사용할 수 있기 때문에 서버 유지
비용도 줄일 수 있을 것이다.


by 미노 | 2009/10/14 13:13 | 델파이 | 트랙백 | 덧글(0)

min hash를 이용한 유사 문서 판별 프로그램

이전에는 유사문서 판별 알고리즘으로 I-Match를 사용했는데, 이번에 shingling과 min hash를 이용해서
유사 문서 판별 프로그램을 만들었다.


위의 그림에서 알 수 있듯이 약 5500여개의 소스 파일을 대상으로 유사 문서 판별을 하는데 16분 정도 소요가 되었다.
위 결과는 메모리 해시 테이블을 이용했을 경우인데, 만일 디스크 기반 해시 방법으로 했을 때는 40분 이상 걸렸다.
대용량 문서를 고려하면 디스크 기반으로 갈 수 밖에 없는데 , 그럴 경우 디스크 I/O로 인해 시간이 훨씬 더 걸릴 수
밖에 없을 것이다. 디스크 기반 해시 알고리즘을 최적화해서 시간을 단축시켜봐야겠다.
min hash가 확률을 바탕으로 한 것이라 반복 실행시 결과가 다르게나온다는 단점이 있지만, 유사 문서 판별 결과는
무난한 편이다.



by 미노 | 2009/10/11 13:45 | 검색엔진 | 트랙백 | 덧글(0)

StandardAnalyzer에서 아쉬운 점들

Lucene에서 분석기로 가장 많이 사용하는 것이 StandardAnalyzer일 것이다.
그런데 StandardTokenizer에서 토큰으로 사용되는 기호가 몇가지 안되기 때문에
C++, C# 모두 C로 토큰처리한다. 그리고 http://www.example.com과 같은
url도 토큰으로 인식하지 못한다.
그래서 예전에 만들었던 StandardTokenizer 을 가지고 위의 요구를 만족시키는
토큰나이저를 만들었다. 그리고 기호를 시작하는 몇몇 토큰도 인식하도록 했다.
대신에 StandardTokenizer에서 인식하는 NUM형 중의 몇가지 유형은 구현의
복잡성 때문에 지원하지 않도록 했다.

이걸 색인에 적용하기 위해서는 그동안 색인된 걸 재색인을 해야하니 색인에
시간이 많이 소요될 것 같다.




by 미노 | 2009/09/08 14:40 | 검색엔진 | 트랙백 | 덧글(0)

OpenSearch를 이용해 브라우저에 검색사이트 등록

파이어폭스 주소창 오른쪽에 검색을 할 수 있는 검색 입력창이 있는데,
여기에 자신의 검색사이트를 등록할 수 있는 OpenSearch라는 표준이 있다.
먼저 OpenSearch 형식에 맞는 xml 파일을 작성한다.
예를들어 내가 운영하고 검색사이트의 경우 아래와 같이 작성하였다.

<?xml version="1.0" encoding="UTF-8" ?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>DevSearch</ShortName>
<Description>개발자를 위한 검색엔진</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16">http://www.devsearch.co.kr/images/favicon.ico</Image>
<Url type="text/html" method="GET" template="http://www.devsearch.co.kr/SearchCategory.aspx">
  <Param name="query" value="{searchTerms}"/>
</Url>
<SearchForm>http://www.devsearch.co.kr/</SearchForm>
</OpenSearchDescription>

그 다음에 메인페이지의 head 태그 밑에 아래와 같은 link 태그를 추가한다.
(위의 xml를 OpenSearch.xml으로 저장한 경우)

<link rel="search" type="application/opensearchdescription+xml" title="DevSearch" href="OpenSearch.xml" />

그러면 해당 사이트에 접속했을 때 브라우저의 검색사이트 드롭다운 메뉴에 해당 사이트 추가 메뉴가 나타나고
추가를 누르면 해당 사이트를 이용해 브라우저에서 검색을 할 수 있게 된다.


OpenSearch는 파이어폭스 뿐만아니라 IE도 역시 지원한다. 파이어폭스는 OpenSearch말고 MozSearch라는
자체의 표준이 있는데 , 이것은 IE에서 지원하지 않으므로 OpenSearch형식으로 만드는 게 더 좋을 것이다.


by 미노 | 2009/09/01 15:26 | 트랙백 | 덧글(0)

RSS 수집 로봇 만들기

예전에 이미 델파이로 RSS 수집 로봇을 만들어 이미 사용하고 있지만
파이썬을 이용해서 RSS 수집 로봇을 한번 만들어 보기로 했다.
파이썬에서는 FeedParser이라는 라이브러리가 RSS 수집에 필요한 기능을
다 제공하므로 훨씬 쉽게 RSS를 수집할 수 있다.
FeedParser는 http://www.feedparser.org/에서 구할 수 있다.


import os, time, urllib2
import feedparser
import logfile

class RSSRobot:
    def __init__(self, rssfile, savedir):
        self.rssfile = rssfile
        self.savedir = savedir
        #assert os.path.exists(savedir), "save directory is not exists!"
        if not os.path.exists(savedir):
            os.makedirs(savedir)
        self.err = open(os.path.join(self.savedir, "error.log"), "a+")

    def __del__(self):
        self.err.close()

    # 에외가 발생하면 예외가 발생한 link를 로그에 기록한다.
    def __addError(self, msg):
        self.err.write("[%s]%s\n" %(time.strftime("%Y-%m-%d %H:%M:%S"), msg))

    # 파일에서 피드 url 목록을 받아온다.
    def __getFeeds(self):
        f = open(self.rssfile)
        feeds = f.readlines()
        f.close()
        return feeds

    # 피드의 아이템 정보를 사전 형식으로 돌려준다.
    def __feedItem(self, item):
        article = {}
        article["link"] = item.link
        if item.has_key("created"): article["created"] = time.strftime("%Y-%m-%d %H:%M:%S", item.created_parsed)
        elif item.has_key("published"): article["created"] = time.strftime("%Y-%m-%d %H:%M:%S", item.published_parsed)
        else: article["created"] = ""

        if item.has_key("updated") and (item.updated_parsed != None):
            article["updated"] = time.strftime("%Y-%m-%d %H:%M:%S", item.updated_parsed)
        article["title"] = item.title
        if item.has_key("summary"): article["summary"] = item.summary
        else: article["summary"] = ""
        return article

    def __readFeeds(self):
      articles = []

      feedlist = self.__getFeeds()
      count = 1
      urls = logfile.LogFile(os.path.join(self.savedir, "CrawedUrls.log"))
      for feed in feedlist:
          url = feed.rstrip()
          print(u"피드 파싱 중... %s(%d / %d)" %(url, count, len(feedlist)))
          try:
              f = feedparser.parse(url)
          except:
              self.__addError("feed parsing error - %s" %url)
          else:
              for e in f.entries:
                  article = self.__feedItem(e)
                  # 전에 수집한 url은 다시 수집하지 않는다.
                  if urls.isExists(article["link"]): break
                  articles.append(article)
                  # 수집한 url을 기록한다.
                  urls.add(article["link"])

          count += 1

      return articles

    # url의 내용을 받아온다.
    def __getURLContent(self, url):
        time.sleep(self.delay)
        try:
            content = urllib2.urlopen(url).read()
            return content
        except:
            self.__addError("__getURLContent error - %s" %url)
            return ""

    ## 피드 아이템 정보를 파일로 저장한다.
    def __saveItem(self, item, filename):
        f = open(filename, "w")
        try:
            f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
            f.write("<Document>\n")
            f.write("<Document.URL> %s </Document.URL>\n" %(item["link"]))
            f.write("<Document.Date> %s </Document.Date>\n" %(item["created"]))
            f.write("<Document.Title> %s </Document.Title>\n" %(self.__encode(item["title"])))
            f.write("<Document.Summary> %s </Document.Summary>\n" %(self.__encode(item["summary"])))
            content = self.__getURLContent(item["link"])
            f.write("<Document.Contents> %s </Document.Contents>\n" %(content))
            f.write("</Document>")
        except:
            print(item["link"])
            self.__addError(item["link"])
        f.close()

    def __encode(self, text):
        return text.encode('utf8')

    ## RSS 문서를 저장할 디렉토리(yyyymmdd\hhmm)를 생성한다.
    def __makeDir(self, dir):
        if not os.path.exists(dir):
            os.makedirs(dir)

    def execute(self, delay=1):
        self.delay = delay
        print(u"수집시작... %s" %time.strftime("%Y-%m-%d %H:%M:%S"))
        t1 = time.time()
        articles = self.__readFeeds()
        count = 0
        savepath = "%s\\%s" %(self.savedir, time.strftime("%Y%m%d\\%H%M"))
        self.__makeDir(savepath)
        for article in articles:
            count += 1
            print("%s (%d)" %(article["link"], count))
            filename = "%s\\%d.xml" %(savepath, count)
            self.__saveItem(article, filename)
        print(u"수집종료... %s" %time.strftime("%Y-%m-%d %H:%M:%S"))
        t2 = time.time()
        print(u"소요시간... %s초" %str(t2 - t1))


if __name__ == "__main__":
    robot = RSSRobot("blog.txt", "f:\\document\\test")
    robot.execute()


by 미노 | 2009/08/03 14:08 | 파이썬 | 트랙백 | 덧글(2)

Lucene 색인을 DB로 변환하기

관리상의 이유로 루씬 색인을 DB로 변환했으면 하는 생각이 들 때가 있다.
그래서 PyLucene를 이용해서 Lucene 색인을 DB로 변환하는 모듈을 작성했다.
이 모듈을 사용하기 위해서는 먼저 PyLucene가 설치되어 있어야 한다.
PyLucene에 대한 자료는 "파이썬 3 Programming" 라는 책에 잘 설명이 되어 있다.


##Lucene 색인을 데이터베이스로 변환

import sqlite3, os.path

os.environ['PATH'] = os.path.join(os.environ['JAVA_HOME'], r'jre\bin\client') + ';' + os.environ['PATH']

import lucene

# 색인의 필드 목록을 구함
def index_fields(indexdir):
    reader = lucene.IndexReader.open(indexdir)
    fields = reader.getFieldNames(lucene.IndexReader.FieldOption.ALL)
    names = []
    for field in fields:
        names.append(field)
    reader.close()
    return names


#색인 필드 목록을 이용해 테이블 생성
#DB에 테이블이 이미 존재하면 테이블을 삭제 후 생성
def create_table(db, table, fields):
    con = sqlite3.connect(db)
    cur = con.cursor()
    s = ""
    for i in range(len(fields)):
        if i == len(fields)-1:
            s = s + fields[i] + " text"
        else:
            s = s + fields[i] + " text, "
    #sql = "CREATE TABLE {0} ({1});".format(table, s)
    sql = "CREATE TABLE %s (%s);" %(table, s)
    print sql
    if os.path.exists(db):
        try:
            cur.execute("DROP TABLE %s" %table)
        except:
            pass
    cur.execute(sql)

# insert 문장(INSERT INTO 테이블명 (필드목록) VALUES (형식인수);)
def insert_sql(table, fields):
    s = ""
    for i in range(len(fields)):
        if i == len(fields)-1:
            s = s + fields[i]
        else:
            s = s + fields[i] + ","
    param = '?,' * (len(fields)-1) + "?"
    return "INSERT INTO %s (%s) VALUES ( %s );" %(table, s, param)

# 문서 doc의 필드값을 리스트 형식으로 돌려준다.
def doc_values(doc, fields):
    values = []
    for i in range(len(fields)):
        value = doc.get(fields[i])
        values.append(value)
    return values

#색인을 DB에 복사
def export_index(indexdir, db, table):
    reader = lucene.IndexReader.open(indexdir)
    count = lucene.IndexReader.numDocs(reader);
    print count
    con = sqlite3.connect(db)
    # 트랜잭션 처리를 하지 않고 자동 커밋 처리
    #con.isolation_level = None
    cur = con.cursor()
    # 색인의 필드 목록을 받아온다.
    fields = index_fields(indexdir)
    sql = insert_sql(table, fields)
    print sql
    for i in range(count):
        doc = lucene.IndexReader.document(reader, i)
        cur.execute(sql, doc_values(doc, fields) )
    con.commit()
    reader.close()

def select_table(db, table):
    con = sqlite3.connect(db)
    cur = con.cursor()
    cur.execute("SELECT COUNT(*) FROM %s;" %table)
    #cur.fetchall()
    #print cur.rowcount
    for row in cur:
        print row

# 경로의 마지막명을 돌려준다.(c:\dir1\dir2 => dir2)
def extract_path(path):
    if path.endswith('\\'):
        path = path[0:-1]
    idx = path.rfind('\\')
    if idx == -1: return ""
    else:
        s = path[idx+1:]
        return s

#Lucene 색인을 데이터베이스로 변환
def index2db(indexdir, db):
    lucene.initVM(lucene.CLASSPATH)
    if not os.path.exists(indexdir):
        print u"색인(%s)이 존재하지 않습니다." %(indexdir)
        return
    table_name = extract_path(indexdir)
    fields = index_fields(indexdir)
    create_table(db, table_name, fields)
    export_index(indexdir, db, table_name)

if __name__ == "__main__":
    indexdir = "e:\\index\\web"
    db = "c:\\test.db"
    index2db(indexdir, db)
    select_table(db, extract_path(indexdir))


DB는 파이썬에서 지원되는 SQLite3를 이용하는데 속도가 무척 빠르네요.


by 미노 | 2009/07/26 14:54 | 파이썬 | 트랙백 | 덧글(0)

통합검색시 색인의 구성

통합검색시 검색 속도가 생각보다 느려서 원인을 분석해 보았다.
예를들어 웹문서,블로그,뉴스에 대한 통합검색을 한다고 해보자.
각각의 검색에 대해 0.5초가 걸린다면 통합검색에 걸리는 순수 시간은 1.5초가 될 것이다.
이러한 속도 문제를 해결하기 위해선 멀티쓰레드를 이용해서 통합검색을 구현하면 속도를
줄일 수 있지 않을까 기대해 볼 수 있다. 실제로 쓰레드(비동기)를 이용해 검색을 해보니
순차적으로 검색하는 것과 별반 차이가 없는 결과가 나왔다.
원인을 분석하다 보니 디스크 I/O에 의한 병목 현상인 것으로 나타났다. 색인을 하나의
디스크에 모두 관리하고 있었는데 쓰레드가 여러개라 하더라도 디스크 자원은 하나라서
여기서 병목 현상이 발생했던 것이다. 그래서 색인을 두 개의 디스크로 분리하고 검색해 보니
눈에 띄는 속도 향상이 있었다.
결론적으로 색인은 물리적으로 분산시켜 놓는 것이 통합검색에 유리하다는 것이다.

by 미노 | 2009/07/08 14:52 | 검색엔진 | 트랙백 | 덧글(0)

BITNAMI 이거 물건이네요.

그동안 위키를 직접 사용해 볼려고 했는데 설치가 까다로워서 제대로 설치를
할 수 없었다. 그런데 이런 오픈소스를 실행파일 하나로 통합해서 자동으로 설치해 주는
프로그램이 있다는 걸 알았다. BITNAMI가 그 주인공이다.
여기서 DocuWiki와 Trac 설치 프로그램을 받아서 실행하니 원클릭으로 설치가 되었다.
이럴수가! 이거 대박이네요.
이 외에도 Drupal, Phpbb, MediaWiki 등 많은 오픈소스 툴 설치를 지원하니 그동안
오픈소스 툴 설치하기가 힘들어서 고민하시는 분은 가서 구경해 보세요. 강추입니다.



by 미노 | 2009/04/29 20:51 | 트랙백 | 덧글(0)

문서의 인코딩 알아내기

델파이 2009에서 유니코드를 지원하기 때문에 TEncoding.GetBufferEncoding를 이용해 문서의
인코딩을 알 수 있다. 하지만 TEncoding.GetBufferEncoding 메서드는 BOM를 기준으로 유니코드를
판단하기 때문에 BOM없는 UTF8은 인식하지 못한다. 그래서 이 문제를 해결하기 위해
UniSynEdit에 있는 유니코드 관련 부분을 짜집기해서 문서의 인코딩을 구별하는 함수를 만들어봤다.

// checks for a BOM in UTF-8 format or searches the first 4096 bytes for
// typical UTF-8 octet sequences
function IsUTF8(Stream: TStream; out WithBOM: Boolean): Boolean;
const
  MinimumCountOfUTF8Strings = 1;
  MaxBufferSize = $4000;
var
  Buffer: array of Byte;
  BufferSize, i, FoundUTF8Strings: Integer;

  // 3 trailing bytes are the maximum in valid UTF-8 streams,
  // so a count of 4 trailing bytes is enough to detect invalid UTF-8 streams
  function CountOfTrailingBytes: Integer;
  begin
    Result := 0;
    inc(i);
    while (i < BufferSize) and (Result < 4) do
    begin
      if Buffer[i] in [$80..$BF] then
        inc(Result)
      else
        Break;
      inc(i);
    end;
  end;

begin
  // if Stream is nil, let Delphi raise the exception, by accessing Stream,
  // to signal an invalid result

  // start analysis at actual Stream.Position
  BufferSize := Min(MaxBufferSize, Stream.Size - Stream.Position);

  // if no special characteristics are found it is not UTF-8
  Result := False;
  WithBOM := False;

  if BufferSize > 0 then
  begin
    SetLength(Buffer, BufferSize);
    Stream.ReadBuffer(Buffer[0], BufferSize);
    Stream.Seek(-BufferSize, soFromCurrent);

    { first search for BOM }

    if (BufferSize >= Length(UTF8BOM)) and CompareMem(@Buffer[0], @UTF8BOM[0], Length(UTF8BOM)) then
    begin
      WithBOM := True;
      Result := True;
      Exit;
    end;

    { If no BOM was found, check for leading/trailing byte sequences,
      which are uncommon in usual non UTF-8 encoded text.

      NOTE: There is no 100% save way to detect UTF-8 streams. The bigger
            MinimumCountOfUTF8Strings, the lower is the probability of
            a false positive. On the other hand, a big MinimumCountOfUTF8Strings
            makes it unlikely to detect files with only little usage of non
            US-ASCII chars, like usual in European languages. }

    FoundUTF8Strings := 0;
    i := 0;
    while i < BufferSize do
    begin
      case Buffer[i] of
        $00..$7F: // skip US-ASCII characters as they could belong to various charsets
          ;
        $C2..$DF:
          if CountOfTrailingBytes = 1 then
            inc(FoundUTF8Strings)
          else
            Break;
        $E0:
          begin
            inc(i);
            if (i < BufferSize) and (Buffer[i] in [$A0..$BF]) and (CountOfTrailingBytes = 1) then
              inc(FoundUTF8Strings)
            else
              Break;
          end;
        $E1..$EC, $EE..$EF:
          if CountOfTrailingBytes = 2 then
            inc(FoundUTF8Strings)
          else
            Break;
        $ED:
          begin
            inc(i);
            if (i < BufferSize) and (Buffer[i] in [$80..$9F]) and (CountOfTrailingBytes = 1) then
              inc(FoundUTF8Strings)
            else
              Break;
          end;
        $F0:
          begin
            inc(i);
            if (i < BufferSize) and (Buffer[i] in [$90..$BF]) and (CountOfTrailingBytes = 2) then
              inc(FoundUTF8Strings)
            else
              Break;
          end;
        $F1..$F3:
          if CountOfTrailingBytes = 3 then
            inc(FoundUTF8Strings)
          else
            Break;
        $F4:
          begin
            inc(i);
            if (i < BufferSize) and (Buffer[i] in [$80..$8F]) and (CountOfTrailingBytes = 2) then
              inc(FoundUTF8Strings)
            else
              Break;
          end;
        $C0, $C1, $F5..$FF: // invalid UTF-8 bytes
          Break;
        $80..$BF: // trailing bytes are consumed when handling leading bytes,
                   // any occurence of "orphaned" trailing bytes is invalid UTF-8
          Break;
      end;

      if FoundUTF8Strings = MinimumCountOfUTF8Strings then
      begin
        Result := True;
        Break;
      end;

      inc(i);
    end;
  end;
end;


  function GetEncoding: TEncoding;
  var
    BytesRead: Integer;
    ByteOrderMask: array[0..5] of Byte; // BOM size is max 5 bytes (cf: wikipedia)
    WithBOM: Boolean;
  begin
    Result:= TEncoding.Default;
    Stream.Position:= 0;
    BytesRead := Stream.Read(ByteOrderMask[0], SizeOf(ByteOrderMask));

    // UTF16 LSB = Unicode LSB/LE
    if (BytesRead >= 2) and (ByteOrderMask[0] = UTF16BOMLE[0])
      and (ByteOrderMask[1] = UTF16BOMLE[1]) then
      Result:= TEncoding.Unicode;

    // UTF16 MSB = Unicode MSB/BE
    if (BytesRead >= 2) and (ByteOrderMask[0] = UTF16BOMBE[0])
      and (ByteOrderMask[1] = UTF16BOMBE[1]) then
      Result:= TEncoding.BigEndianUnicode;

    // UTF8
    if (BytesRead >= 3) and (ByteOrderMask[0] = UTF8BOM[0])
      and (ByteOrderMask[1] = UTF8BOM[1]) and (ByteOrderMask[2] = UTF8BOM[2]) then
      Result:= TEncoding.UTF8;

    // default case (Ansi)
    if Result = TEncoding.Default then
    begin
      if IsUTF8(Stream, WithBom) then
        Result:= TEncoding.UTF8;
    end;
  end;


GetEncoding 함수로 문서의 인코딩을 알 수 있는데 UTF32는 지원하지 않는다.
델파이 2009도 그렇고 UniSynEidt도 UTF32 형식은 검사하지 않는데, 아마도
문자당 4바이트라서 현실적으로 이 형식을 사용하는 문서가 없을거라는 점 때문인 것 같다.

by 미노 | 2009/04/27 11:24 | 델파이 | 트랙백 | 덧글(0)

◀ 이전 페이지다음 페이지 ▶