 |  |
|
RSS 내용 파싱하기
RSS 내용은 본질적으로 또 다른 XML 방언이다. 모든 언어에는 DOM 또는 SAX 프로세스를 처리하는 많은 복잡한 라이브러리와 함께 XML을 분석하는 여러 방법이 있다. PHP도 그 중 하나다. 하지만 PHP의 강력한 정규식 지원을 사용해 RSS를 분석하는 간단한 접근을 시도할 것이다. 정규식은 저주같아 보이겠지만 텍스트를 다루는 능력은 최고다.
정규식 사용하기
본 튜토리얼에서 다양한 파싱 방법 중 하나로 쉽고 빠른 정규식을 사용할 것이다. 이는 빌트인이고 간단한 사용 메커니즘으로 PHP의 DOM 라이브러리와는 상관 없다. 물론 완전한 문서 트리, 스키마 검증, 공백에 둔감 등 DOM의 장점이 많지만 본 튜토리얼은 이미 굉장히 앞선(avant garde) 개념을 보여줬으므로 하나 더 보여준다고 크게 나쁠 것 없다. DOM과 SAX 모두 정규식으로 기본 XML을 분석한다.
이전에 정규식을 사용해 봤다면 "@#%#$" 같은 것이겠지만(이것 또한 정규식이다) 정규식 사용을 최소로 하고 단일 함수로 감싸 요소를 가져올 것이다.
요소 페처 만들기
함수를 사용하여 RegExp 로직을 감싸는 것은 인터페이스에서("그 요소를 달라") 요소를 가져오는(RegExp를 사용하여) 실제 구현을 추상화하므로 말이 된다. 연습으로 DOM을 사용하여 이 함수를 다시 짜려는 독자도 있을 것이다.
어떤 문서나 피드에서 특정 XML 요소를 가져오기 위해 다음 함수를 접근 가능한 PHP 파일에 넣는다. Listing 5를 보자.
Listing 5. XML 문서에서 요소 찾아오는 PHP 함수
function get_elements($xml, $element,$multiple)
{
if ($multiple) {
/* Return ALL matches of this element in the $xml provided.
* Results will be returned as an array or arrays
*/ if ($multiple) {
$item = preg_match_all("#<$element(|[^>]*)>(.*)</$element>#msU",
$xml,
$matches,PREG_PATTERN_ORDER);
/* Trim whitespace. This could've been done in the RegExp, */
/* but you can also use PHP's trim() function for that. */
for ($i = 0;
$i < count ($matches[2]);
$i++)
{
$matches[2][$i] = trim ($matches[2][$i]);
} /* end for */
}
else
{
$item = preg_match("/<$element(|[^>]*)>(.*)<\/$element>/msU",
$xml,
$matches);
$matches[2] = trim($matches[2]);
}
/*
* When you get here, $matches is either an array of arrays, or a
* single array, in the following format:
*
* [0] => Entire pattern matched
* [1] => "", or attributes of this element (if any) (| [^>]*)
* [2] => Element content (corresponding to (.*)
*
* You thus choose to return [2] = Element contents, or array of
* matched elements' contents.
*/
return ($matches[2]); /* Element content */
}
|
보면 알 수 있듯이 여기서 어려운 점은 올바른 정규식만 만드는 것이다. 이 경우 정규식은 #<$element(| [^>]*)>(.*)</$element>#msU다.
실제 RegExp는 임의 문자에 의해(대체로 /를 사용하지만 정규식 자체에 /가 필요하므로 #를 선택하는 것이 좋다) 묶인다. 이를 표 2처럼 나눌 수 있다.
표 2. 요소 RegExp 분해
| RegExp 부분 | 뜻 |
|---|
| <$element | 요소를 여는 태그—요소 이름. 여기서 <은 말 그대로 더 작은(<)이며 $element는 PHP에 의해 삽입될 것이다. | | (|[^>]*) | 어떤 것이든 >으로 끝난다. | | > | 요소보다 큰(>) 것으로 끝난다. | | (.*) | 요소 내용 | | <\/$element> | 요소를 끝내는 태그 |
식을 따르는 것은 표 3처럼 행동 변경자를 따르는 것이다.
표 3. 요소 RegExp 변경자 설명
| 변경자 | 뜻 |
|---|
| M | 다중행 매칭. 아니라면 "\n"은 행을 끝냄 | | S | "\n"을 포함, 문자를 매치하기 위해 "."를 설정 | | U | "ungreedy" 설정. 기본 매칭이 "greedy"(위 정규식에서 ".*"로 요소의 닫는 태그와 실제로 매치할 수 있다)고 각 매치에 요소 하나만 원하기 때문에 필요하다. |
이제 이 기능을 전체 애플리이션에 포함시키자.
모두 함께 모으기
이제 RSS 피드를 파싱해 데이터를 얻는 것은 쉬워진다. Listing 6처럼 호출을 get_elements()로 둘러싼다.
Listing 6. 자식 노드와 값으로 특정 요소를 찾아오는 PHP 함수
function get_specific_element($feed, $child, $value)
{
$items = get_elements($feed, "item",true);
if (!$items) { echo "What are you feeding me? No items here!\n";
return(false);}
for ($i = 0 ; $i < count ($items);$i++){
/* to get child elements, simply call on parent element,
* with child name as argument
*/
$val = (get_elements($items[$i],"$child",false));
if ($val == trim($value)) {return ($items[$i]); } ;
}
return(false);
}
|
그리고 이 함수를 호출하려면 Listing 7만 사용하면 된다.
Listing 7. PHP를 사용하여 RSS 데이터베이스 처리하기
$feed = get_channel ($feed_url);
$desired_child = /* Whatever, obtain from user */;
$desired_value = /* Whatever, obtain from user */;
$elem = get_specific_element($feed,
$desired_child,
$desired_value);
/* Have some fun with this element: */
$title = (get_elements($elem,"title",false));
$description = (get_elements($elem,"description",false));
/* do something with title and description */
...
/* get zero or more connections (axons) this element has ..*/
$axons = (get_elements($elem,"mesh:axon",true));
|
간단히 말하자면 찾고 있는 특정 값을 사용하여 특정 아이템 요소를 찾을 것이다. 여기서 제목과 설명을 끌어낼 수 있고 축색돌기의 폼이라면 추가 정보로 가는 링크를 끌어낼 수도 있다.
RDF와 RSS 모두에서 채널에 각각 <title>과 <description>이 있는 개별 <item> 요소를 가지고 있음을 상기하자. 그리고 이것이 DOM/SAX와는 다른 RegExp 처리 방식의 주요 장점이라 하겠다. 즉, RegExp는 텍스트와 매치되고 문서 구조와 상관이 없기 때문에 <item> 요소가 <channel>의 자식 요소든 이것과 분리되든 상관이 없다는 것이다.
요소 찾아오기
데이터베이스를 정의하고 파서 함수를 만들었으니 이제 즐길 차례다. 관계를 살피고 스프링필드(Springfield)의 약속, 사람과 장소를 찾을 뿐만 아니라 사람들 간의 연관과 링크를 따라가볼 수 있다. 로컬 피드로 작업할 것이기 때문에 다음 함수를 사용하여 로컬 파일의 모든 RSS 내용을 취합할 수 있다. Listing 8을 보자.
Listing 8. RSS 데이터베이스를 메모리로 읽기
function get_feed($url)
{
/* open the feed locally */
$fp = fopen ($url, "r");
$content = fread($fp, filesize($url));
return ($content);
} |
그리고 이를 Listing 9처럼 사용한다.
Listing 9. RSS 데이터베이스를 메모리로 읽기(계속)
$contacts_feed = get_feed ("simpsons_contacts.xml");
$engagements_feed = get_feed ("simpsons_engagements.xml");
$places_feed = get_feed ("simpsons_places.xml");
|
메모리에 피드가 있으니 이제 실제 처리할 사항들을 살펴보자.
전통적 룩업
전통적(Classic)이라는 것은 저장소의 요소를 살펴보는 옛 방법을 말한다. 전통적으로 SQL에서 SELECT statement FROM some_table WHERE some condition 같은 것이 그것이다. 같은 기능을 흉내낼 수 없다면 쓸모 없는 데이터베이스가 될 것이다.
예를 들어 Listing 10의 코드를 가지고 여기서 SELECT * from ENGAGEMENTS로 해야 할 것이다.
Listing 10. SELECT 문 흉내내기
$engs = get_elements($engagements_feed, "item",true);
foreach ($engs as $eng)
{
$title = (get_elements($eng,"title",false));
echo ("Got Engagement: $title\n");
}
|
참석자와 함께 모든 약속 목록을 갖고 싶다면 무엇을 해야 하는가? SQL에서는 복잡한 JOIN을 하겠지만 여기서는 간단히 축색돌기를 따르면 된다. Listing 11을 보자.
Listing 11. JOIN된 SELECT 문 흉내내기
$engs = get_elements($engagements_feed, "item",true);
foreach ($engs as $eng)
{
$title = (get_elements($eng,"title",false));
echo ("Got Engagement: $title\n");
$axons = (get_elements($eng,"mesh:axon",true));
foreach ($axons as $ax)
{
if (strstr($ax, "Simpsons:/People")) {
/* Lookup this person */
$part = get_specific_element($contacts_feed,
"guid",
$ax);
if (!$part) { echo ($ax . " Not found\n");}
else {
$title = (get_elements($part,"title",false));
echo ("Participant: $title\n");
}
}
}
|
이 샘플은 http://www.hisown.com/rss/lookuptest.php에서 받을 수 있다.
임무를 완수했다. 이제 테이블 기반 데이터베이스로 할 수 있는 일은 뭐든 할 수 있다. 하지만 개선할 수는 없을까?
샘플 결합
예전에 만든 질의에는 새로울 것 없었고 결국 어느 관계형 데이터베이스 테이블이든 질의를 수행할 수 있었다. 하지만 더 나아가 이제 새로운 타입의 질의인 결합적 질의(아이템이 가져야 하는 것은 비슷함)를 사용할 수 있다. SQL과 테이블에서 이를 행하는 것은 불가능하진 않지만 매우 힘든 작업이다. 하지만 새 결합적 포맷으로 행하는 것은 간단하다. Listing 12를 보자.
Listing 12. 결합적 질의
$contacts_feed = get_feed ("simpsons_contacts.xml");
$contacts = get_elements($contacts_feed,"item",true);
$common_axons = Array();
foreach ($contacts as $contact)
{
$title = get_elements($contact,"title",false);
echo ("Got contact: $title<BR/>\n");
$axons = (get_elements($contact,"mesh:axon",true));
foreach ($axons as $ax)
{
/* Lookup these axons in the $common_axons */
$cax_count = count ($common_axons);
for($cax_no = 0;
$cax_no < $cax_count;
$cax_no++)
{
if ($common_axons[$cax_no] == $ax)
{
echo ("Bingo! Commonality found: $ax\n");
exit(0);
}
}
/* Add anyway */
$common_axons [count($common_axons)] = $ax;
}
} /* end foreach*/
|
그리고 예상대로 결과는 Listing 13에서 볼 수 있다.
Listing 13. 결과
Got contact: Homer Jay Simpson
Got contact: Charles Montgomery_Burns
Bingo! Commonality found: Simpsons:/Places/Springfield_Nuclear_Power_Plant
|
첨부된 샘플은 http://www.hisown.com/rss/commontest.php에서 얻을 수 있다.This sample is attached, and available at
그래프 이론(Graph Theory)에서 보자면 지금까지 한 것은 너비우선탐색(Breadth First Search, BFS)과 매우 비슷하다. 이는 정말 빙산의 일각일 뿐이다. 한 단계 아래로 갔을 뿐이고 축색돌기는 일정 방향이다. 진정한 결합은 깊이와 양방향 링크를 이용한다. 또한 특정 검색이나 그 이상에 사용할 수 있는 링크 타입도 무시했다. 이것은 정보 처리와 검색, 그리고 클러스터링같은 그래프 이론의 계산에 관한 문제와 같은 다양한 분야에서 매우 유용하다. 여기서 시도할 수 있는 검색의 또 다른 타입은 깊이우선탐색(Depth First Search)으로 이는 형제 노드보다 자식 노드 결합을 재귀적으로 따른다. 더 자세한 정보는 컴퓨터 과학 교과서의 알고리듬 부분을 참조하기 바란다(참고자료의 링크를 보자).
가끔 결과에서 최소 파싱만을 수행하고 이를 XML 폼으로 남기고자 할 때가 있다. 이는 특히 웹 서비스에서 유용하다. 이제 이를 추가 비용 없이 수행하는 방법에 대해 다뤄 보겠다. 사실 결과 그대로 남겨두는 게 더 간단하다. XML 조각을 쉽게 재조립하여 유효한 새 문서로 만들 수 있다.
|  |