※ Down: SIXP.php (PHP5)
제가 PHP5의 객체지향 문법을 빌어서 간단히 만들어본 간단한 XML 파서입니다.
처음에, PHP로 웹 프로젝트를 진행하던 중에 HTML의 태그를 필터링하려는 의도로 시작한 것이 어쩌다보니 XML 파서를 구현하게 되버렸습니다.
이 파서는 SAX를 흉내내서 만든 이벤트 기반의 파서입니다. 다만, 이 파서는 태그의 처리에만 중점을 두었기 때문에 < ... > 형식의 태그만 들어가 있다면 어떤 문서든 파싱할 수 있습니다. 태그로 시작해서 태그로 끝난다는 규칙만 지켜준다면 말입니다.
그리고, 본래 XML 보다도 HTML을 처리하려고 만든거다 보니, XML의 규칙따위는 검사안합니다. 게다가 이벤트 기반의 파서다보니 태그를 열어놓고 안닫았다고 해서 오류나거나 그러지도 않습니다. 말그대로 심플한 파서입니다. (우훗...)
그래도, 간단한 RSS 리더기 정도의 구현에는 별 문제 없이 쓸 수 있을 겁니다.
이 파서가 파싱하는 원리는 이렇습니다.
우선, 기본적으로 XML 문서를 다음과 같은 형태로 생각합니다.
여기서 파란색 부분에서 on_tag_begin() 이벤트가 발생하고, 녹색 부분에서 on_data(), 빨간색 부분에서 on_tag_end() 이벤트를 발생시킵니다. 아주 심플하죠?
파서가 해주는 역할은 각 이벤트마다 필요한 정보(태그 이름, 속성, 데이터)를 이벤트 핸들러로 전달해 주는 것 뿐입니다.
그런데 HTML 같은 경우는 문서가 이런식으로 구성되어있는게 보통이죠.
이런 경우의 이벤트 발생을 컬러로 표시하면 다음과 같습니다.
제가 PHP5의 객체지향 문법을 빌어서 간단히 만들어본 간단한 XML 파서입니다.
처음에, PHP로 웹 프로젝트를 진행하던 중에 HTML의 태그를 필터링하려는 의도로 시작한 것이 어쩌다보니 XML 파서를 구현하게 되버렸습니다.
이 파서는 SAX를 흉내내서 만든 이벤트 기반의 파서입니다. 다만, 이 파서는 태그의 처리에만 중점을 두었기 때문에 < ... > 형식의 태그만 들어가 있다면 어떤 문서든 파싱할 수 있습니다. 태그로 시작해서 태그로 끝난다는 규칙만 지켜준다면 말입니다.
그리고, 본래 XML 보다도 HTML을 처리하려고 만든거다 보니, XML의 규칙따위는 검사안합니다. 게다가 이벤트 기반의 파서다보니 태그를 열어놓고 안닫았다고 해서 오류나거나 그러지도 않습니다. 말그대로 심플한 파서입니다. (우훗...)
그래도, 간단한 RSS 리더기 정도의 구현에는 별 문제 없이 쓸 수 있을 겁니다.
사용법 (1)
<?php
// 기본적으로 PHP에 내장된 SAX 파서와 사용법은 비슷합니다.
include_once "SIXP.php";
// 파서 생성
$parser = new Sixp();
// 이벤트 핸들러 설정
$parser->setEventHandler('on_tag_begin', 'on_tag_end', 'on_data');
// 파싱: 파싱할 문자열을 인수로 넘겨준다
$parser->parse($xml_document);
$parser = NULL;
// 이벤트 핸들러
function on_tag_begin($tag, $attrs)
{
// 열리는 태그에서의 처리
// $tag: 태그 이름
// $attrs: ATTRIBUTE 정보가 배열로 넘어온다
}
function on_tag_end($tag)
{
// 닫히는 태그에서의 처리
}
function on_data($data)
{
// $data: 태그와 태그 사이에 있는 데이터
}
?>
<?php
// 기본적으로 PHP에 내장된 SAX 파서와 사용법은 비슷합니다.
include_once "SIXP.php";
// 파서 생성
$parser = new Sixp();
// 이벤트 핸들러 설정
$parser->setEventHandler('on_tag_begin', 'on_tag_end', 'on_data');
// 파싱: 파싱할 문자열을 인수로 넘겨준다
$parser->parse($xml_document);
$parser = NULL;
// 이벤트 핸들러
function on_tag_begin($tag, $attrs)
{
// 열리는 태그에서의 처리
// $tag: 태그 이름
// $attrs: ATTRIBUTE 정보가 배열로 넘어온다
}
function on_tag_end($tag)
{
// 닫히는 태그에서의 처리
}
function on_data($data)
{
// $data: 태그와 태그 사이에 있는 데이터
}
?>
사용법 (2)
<?php
include_once "SIXP.php";
// 파서 생성과 동시에 파싱할 문서(string)를 전달
$parser = new Sixp($xml_document);
// 이벤트 핸들러 설정
$parser->setEventHandler('on_tag_begin', 'on_tag_end', 'on_data');
// --- 옵션 설정 ---
// TRUE이면 모든 태그를 대문자로 변환한다. (기본값)
$parser->option_upperTag(TRUE);
// 지정한 태그만 파싱하도록 설정
// 파싱할 태그들을 배열로 전달하면 된다.
$parser->option_parsingTags(array('TITLE', 'CHENNEL', 'ITEM', 'CATEGORY', 'DESCRIPTION', 'PUBDATE', 'LINK'));
// 파싱
$parser->parse();
.
.
.
<?php
include_once "SIXP.php";
// 파서 생성과 동시에 파싱할 문서(string)를 전달
$parser = new Sixp($xml_document);
// 이벤트 핸들러 설정
$parser->setEventHandler('on_tag_begin', 'on_tag_end', 'on_data');
// --- 옵션 설정 ---
// TRUE이면 모든 태그를 대문자로 변환한다. (기본값)
$parser->option_upperTag(TRUE);
// 지정한 태그만 파싱하도록 설정
// 파싱할 태그들을 배열로 전달하면 된다.
$parser->option_parsingTags(array('TITLE', 'CHENNEL', 'ITEM', 'CATEGORY', 'DESCRIPTION', 'PUBDATE', 'LINK'));
// 파싱
$parser->parse();
.
.
.
이 파서가 파싱하는 원리는 이렇습니다.
우선, 기본적으로 XML 문서를 다음과 같은 형태로 생각합니다.
<TAG ATTRIBUTE1="VALUE" ATTRIBUTE2="VALUE2" ...> DATA </TAG>
여기서 파란색 부분에서 on_tag_begin() 이벤트가 발생하고, 녹색 부분에서 on_data(), 빨간색 부분에서 on_tag_end() 이벤트를 발생시킵니다. 아주 심플하죠?
파서가 해주는 역할은 각 이벤트마다 필요한 정보(태그 이름, 속성, 데이터)를 이벤트 핸들러로 전달해 주는 것 뿐입니다.
그런데 HTML 같은 경우는 문서가 이런식으로 구성되어있는게 보통이죠.
<STRONG>TITLE</STRONG>CONTENT1<BR><IMG SRC="...">CONTENT2<BR>
이런 경우의 이벤트 발생을 컬러로 표시하면 다음과 같습니다.
<STRONG>TITLE</STRONG>CONTENT1<BR><IMG SRC="...">CONTENT2<BR>
예제: test.php - RSS 문서 파싱
<?php
include_once "SIXP.php";
$handle = @fopen('http://sizuha.egloos.com/index.xml', 'r');
if ($handle):
$document = '';
while (!feof($handle))
$document .= fgets($handle, 4096);
fclose($handle);
$parser = new Sixp($document);
$parser->setEventHandler('on_tag_begin', 'on_tag_end', 'on_data');
$parser->parse();
endif;
function on_tag_begin($tag, $attrs)
{
echo "<$tag";
if (count($attrs) != 0)
foreach ($attrs as $key => $value) echo " $key = "$value"";
echo "><br>";
}
function on_tag_end($tag)
{
echo "</$tag>";
}
function on_data($data)
{
echo "$data<br>";
}
?>
<?php
include_once "SIXP.php";
$handle = @fopen('http://sizuha.egloos.com/index.xml', 'r');
if ($handle):
$document = '';
while (!feof($handle))
$document .= fgets($handle, 4096);
fclose($handle);
$parser = new Sixp($document);
$parser->setEventHandler('on_tag_begin', 'on_tag_end', 'on_data');
$parser->parse();
endif;
function on_tag_begin($tag, $attrs)
{
echo "<$tag";
if (count($attrs) != 0)
foreach ($attrs as $key => $value) echo " $key = "$value"";
echo "><br>";
}
function on_tag_end($tag)
{
echo "</$tag>";
}
function on_data($data)
{
echo "$data<br>";
}
?>
SIXP.php
<?php
/*
* SIXP - Sizuha's Simple XML Parser (for PHP5)
* Version: 0.9
* Author: Sizuha
*
* SAX처럼 이벤트 방식으로 동작하는 초간단 XML 파서.
* XML뿐만 아니라 태그가 삽입된 문서면 Ok.
* Encoding은 UTF-8 기준.
*
*/
define('PARSE_ALL_TAGS', NULL);
define('OPEN_TAG', 0);
define('CLOSE_TAG', 1);
define('CDATA_TAG', 2);
class Sixp
{
private $xmldoc;
// EventHandler
private $begin_event;
private $end_event;
private $data_event;
// for Parsing
private $length;
private $pos;
private $data;
private $attrs;
// Option
private $op_uppertag; // 모든 태그를 대문자로 변환 (Default: TRUE)
private $tags; // 파싱할 태그 (Default: PARSE_ALL_TAGS = NULL)
function __construct($xmldoc = '')
{
if ('' != $xmldoc)
$this->setDoc($xmldoc);
$this->pos = -1;
$this->option_upperTag(TRUE);
$this->tags = PARSE_ALL_TAGS;
}
function __destruct()
{
$this->xmldoc = NULL;
}
function option_upperTag($boolean)
{
$this->op_uppertag = $boolean;
}
// $tags는 Array 혹은 NULL 이어야 함.
function option_parsingTags($tags)
{
$this->tags = $tags;
}
function setEventHandler($start_evnet, $end_evnet, $data_event)
{
$this->begin_event = $start_evnet;
$this->end_event = $end_evnet;
$this->data_event = $data_event;
}
private function inRange()
{
if ($this->pos < $this->length and $this->pos >= 0)
return TRUE;
else
return FALSE;
}
private function inTarget($tag)
{
if (is_null($this->tags)) return TRUE;
foreach ($this->tags as $each)
if ($each == $tag) return TRUE;
return FALSE;
}
private function getChar(&$char)
{
$this->pos++;
$char = $this->getLastChar();
return !is_null($char);
}
private function getLastChar()
{
if ($this->inRange())
return $this->xmldoc[$this->pos];
else
return NULL;
}
private function setDoc($xmldoc) {
$this->xmldoc = $xmldoc;
$this->length = strlen($xmldoc);
}
private function end_of_doc()
{
return ($this->pos >= $this->length-1) ? TRUE : FALSE;
}
function parse($xmldoc = '')
{
if ('' != $xmldoc)
$this->setDoc($xmldoc);
$this->data = '';
$this->pos = -1;
$char = '';
while(!$this->end_of_doc()):
$this->getChar($char);
if ('<' == $char)
$this->parse_tag();
else
$this->data .= $char;
endwhile;
}
private function parse_tag()
{
$char = '';
// 태그를 파싱하기 전에 DATA 이벤트 수행
if ('' != $this->data) $this->event_data();
$this->data = '';
$tagtype = OPEN_TAG; // Warring Message 방지용 (Eclips)
$tagname = $this->getTagName($tagtype);
// <![CDATA[ .. ]]> 에 대한 처리
if (CDATA_TAG == $tagtype) {
$this->parse_cdata();
return;
}
// 파싱 대상의 태그가 아니면 DATA로 저장
if (!$this->inTarget($tagname)) {
$this->data .= "<$tagname".$this->getLastChar();
return;
}
// 속성 파싱
$this->attrs = array();
$this->parse_attr();
// Event
switch ($tagtype):
case OPEN_TAG:
$this->event_tag_begin($tagname);
break;
case CLOSE_TAG:
$this->event_tag_end($tagname);
break;
endswitch;
}
private function parse_attr()
{
$char = $this->getLastChar();
while('>' != $char):
// Key
$key = $this->get_key();
if ('' != $key) {
$this->attrs[$key] = '';
// Value
if ($this->getLastChar() == '=') {
$value = $this->get_value();
$this->attrs[$key] = $value;
}
}
$char = $this->getLastChar();
endwhile;
}
private function get_key()
{
$char = '';
$key = '';
while($this->getChar($char)):
if (' ' == $char and '' == $key) continue;
elseif ('>' == $char or '=' == $char or '/' == $char or ' ' == $char) break;
$key .= $char;
endwhile;
return ($this->op_uppertag) ? strtoupper($key) : $key;
}
private function get_value()
{
$char = '';
$value = '';
$in_block = FALSE;
while($this->getChar($char)) {
if (' ' == $char and '' == $value) continue;
elseif (!$in_block and ('>' == $char or '/' == $char or ' ' == $char)) break;
elseif ('"' == $char) {
if ($in_block) {
$this->pos++;
break;
}
else $in_block = TRUE;
}
else $value .= $char;
}
return $vaue;
}
private function getTagName(&$tag_type)
{
$tagname = '';
$tag_type = OPEN_TAG;
$char = '';
while($this->getChar($char)):
if ('![CDATA[' == $tagname) {
$tag_type = CDATA_TAG;
break;
}
elseif ('/' == $char and '' == $tagname)
$tag_type = CLOSE_TAG;
elseif ( (' ' == $char and '' != $tagname) or '/' == $char or '>' == $char )
break;
else
$tagname .= $char;
endwhile;
return ($this->op_uppertag) ? strtoupper($tagname) : $tagname;
}
private function parse_cdata()
{
$char = '';
$char2 = '';
$char3 = '';
while($this->getChar($char)):
if (']' == $char) {
$this->getChar($char2);
$this->getChar($char3);
if (']' == $char2 and '>' == $char3) return;
else $this->data .= $char.$char2.$char3;
}
else
$this->data .= $char;
endwhile;
}
private function event_tag_begin($tagname)
{
$func = $this->begin_event;
$func($tagname, $this->attrs);
}
private function event_tag_end($tagname)
{
$func = $this->end_event;
$func($tagname);
}
private function event_data()
{
$func = $this->data_event;
$func($this->data);
}
}
?>
<?php
/*
* SIXP - Sizuha's Simple XML Parser (for PHP5)
* Version: 0.9
* Author: Sizuha
*
* SAX처럼 이벤트 방식으로 동작하는 초간단 XML 파서.
* XML뿐만 아니라 태그가 삽입된 문서면 Ok.
* Encoding은 UTF-8 기준.
*
*/
define('PARSE_ALL_TAGS', NULL);
define('OPEN_TAG', 0);
define('CLOSE_TAG', 1);
define('CDATA_TAG', 2);
class Sixp
{
private $xmldoc;
// EventHandler
private $begin_event;
private $end_event;
private $data_event;
// for Parsing
private $length;
private $pos;
private $data;
private $attrs;
// Option
private $op_uppertag; // 모든 태그를 대문자로 변환 (Default: TRUE)
private $tags; // 파싱할 태그 (Default: PARSE_ALL_TAGS = NULL)
function __construct($xmldoc = '')
{
if ('' != $xmldoc)
$this->setDoc($xmldoc);
$this->pos = -1;
$this->option_upperTag(TRUE);
$this->tags = PARSE_ALL_TAGS;
}
function __destruct()
{
$this->xmldoc = NULL;
}
function option_upperTag($boolean)
{
$this->op_uppertag = $boolean;
}
// $tags는 Array 혹은 NULL 이어야 함.
function option_parsingTags($tags)
{
$this->tags = $tags;
}
function setEventHandler($start_evnet, $end_evnet, $data_event)
{
$this->begin_event = $start_evnet;
$this->end_event = $end_evnet;
$this->data_event = $data_event;
}
private function inRange()
{
if ($this->pos < $this->length and $this->pos >= 0)
return TRUE;
else
return FALSE;
}
private function inTarget($tag)
{
if (is_null($this->tags)) return TRUE;
foreach ($this->tags as $each)
if ($each == $tag) return TRUE;
return FALSE;
}
private function getChar(&$char)
{
$this->pos++;
$char = $this->getLastChar();
return !is_null($char);
}
private function getLastChar()
{
if ($this->inRange())
return $this->xmldoc[$this->pos];
else
return NULL;
}
private function setDoc($xmldoc) {
$this->xmldoc = $xmldoc;
$this->length = strlen($xmldoc);
}
private function end_of_doc()
{
return ($this->pos >= $this->length-1) ? TRUE : FALSE;
}
function parse($xmldoc = '')
{
if ('' != $xmldoc)
$this->setDoc($xmldoc);
$this->data = '';
$this->pos = -1;
$char = '';
while(!$this->end_of_doc()):
$this->getChar($char);
if ('<' == $char)
$this->parse_tag();
else
$this->data .= $char;
endwhile;
}
private function parse_tag()
{
$char = '';
// 태그를 파싱하기 전에 DATA 이벤트 수행
if ('' != $this->data) $this->event_data();
$this->data = '';
$tagtype = OPEN_TAG; // Warring Message 방지용 (Eclips)
$tagname = $this->getTagName($tagtype);
// <![CDATA[ .. ]]> 에 대한 처리
if (CDATA_TAG == $tagtype) {
$this->parse_cdata();
return;
}
// 파싱 대상의 태그가 아니면 DATA로 저장
if (!$this->inTarget($tagname)) {
$this->data .= "<$tagname".$this->getLastChar();
return;
}
// 속성 파싱
$this->attrs = array();
$this->parse_attr();
// Event
switch ($tagtype):
case OPEN_TAG:
$this->event_tag_begin($tagname);
break;
case CLOSE_TAG:
$this->event_tag_end($tagname);
break;
endswitch;
}
private function parse_attr()
{
$char = $this->getLastChar();
while('>' != $char):
// Key
$key = $this->get_key();
if ('' != $key) {
$this->attrs[$key] = '';
// Value
if ($this->getLastChar() == '=') {
$value = $this->get_value();
$this->attrs[$key] = $value;
}
}
$char = $this->getLastChar();
endwhile;
}
private function get_key()
{
$char = '';
$key = '';
while($this->getChar($char)):
if (' ' == $char and '' == $key) continue;
elseif ('>' == $char or '=' == $char or '/' == $char or ' ' == $char) break;
$key .= $char;
endwhile;
return ($this->op_uppertag) ? strtoupper($key) : $key;
}
private function get_value()
{
$char = '';
$value = '';
$in_block = FALSE;
while($this->getChar($char)) {
if (' ' == $char and '' == $value) continue;
elseif (!$in_block and ('>' == $char or '/' == $char or ' ' == $char)) break;
elseif ('"' == $char) {
if ($in_block) {
$this->pos++;
break;
}
else $in_block = TRUE;
}
else $value .= $char;
}
return $vaue;
}
private function getTagName(&$tag_type)
{
$tagname = '';
$tag_type = OPEN_TAG;
$char = '';
while($this->getChar($char)):
if ('![CDATA[' == $tagname) {
$tag_type = CDATA_TAG;
break;
}
elseif ('/' == $char and '' == $tagname)
$tag_type = CLOSE_TAG;
elseif ( (' ' == $char and '' != $tagname) or '/' == $char or '>' == $char )
break;
else
$tagname .= $char;
endwhile;
return ($this->op_uppertag) ? strtoupper($tagname) : $tagname;
}
private function parse_cdata()
{
$char = '';
$char2 = '';
$char3 = '';
while($this->getChar($char)):
if (']' == $char) {
$this->getChar($char2);
$this->getChar($char3);
if (']' == $char2 and '>' == $char3) return;
else $this->data .= $char.$char2.$char3;
}
else
$this->data .= $char;
endwhile;
}
private function event_tag_begin($tagname)
{
$func = $this->begin_event;
$func($tagname, $this->attrs);
}
private function event_tag_end($tagname)
{
$func = $this->end_event;
$func($tagname);
}
private function event_data()
{
$func = $this->data_event;
$func($this->data);
}
}
?>




덧글
중첩테이블과 텍스트스타일의 복잡한 중첩 경우의 수를 다 카바하지 못해서 그만...
지금 아이디어라면 다시 만들수 있을 것 같은데... 귀차니즘과 두려움이 묘하게 섞인 기분. 시즈하님 소스를 보면서 용기가 약간 생기네요. ^^
>> 큰돌 님
저도 이 파서를 Smalltalk로 포팅하려고 계획하고 있습니다. ^^
>> 달룟 님
제가페인 관련해서 찾아다니다 글 잘 읽고 가구요- 링크신고드립니다-
글을 참 잘 쓰시는 것 같아서 부럽고, 저도 나름 프로그래밍 계열에서 일하고 있는데, 아시는 것이 참 많으신것 같아서 부럽습니다^^;;
>> Sikuru 님
저도 그렇게 생각합니다. ^^
>> 달룟 님
이것도 정리해서 올려보겠습니다.