Archive

Posts Tagged ‘xml’

iPhone, XML 처리 예제 – NSXMLParser 이용하기

3월 10th, 2010

몇일 전 iPhone에서 HTTP로 XML 데이터 요청하는 예제를 올렸다가 iPhone SDK에서 NSXMLDocument 클래스를 지원하지 않는 다는 것을 확인하고 NSXMLParser 클래스를 이용해서 XML 데이터를 처리하는 예제를 다시 준비했습니다. 아마도 iPhone에서 NSXMLDocument를 지원하지 않는 건 NSXMLDocument가 메모리를 많이 먹기 때문인 아닐까 싶습니다.

NSXMLParser는 XML 문서를 순차적으로 분석(parse)하면서 이벤트별 처리를 위임(delegate)하는 방식입니다. 따라서 NSXMLDocument를 사용하는 예제보다는 훨씬 더 복잡합니다. 참고로 iPhone에서 사용할 수 있는 XML 관련 라이브러리들이 개발되어 있기 때문에 해당 라이브러리를 익혀서 사용하는 것이 개발 효율은 더 좋을 것 같습니다. 하지만 NSXMLParser를 이용해서 구현하면 메모리도 적게 먹고 나름 성능 향상을 기대할 수 기대할 수 있기 때문에 직접 작성하는 것도 알아두면 좋을 듯 합니다.

예제에 포함된 파일 목록은 아래와 같습니다.

파일 설명
showrooms.xml XML 파일
Dealer.h
Dealer.m
Dealer 모델 클래스
Showroom.h
Showroom.m
Showroom 모델 클래스
ShowroomsXmlParse.h
ShowroomsXmlParse.m
showrooms.xml용 파서 클래스
showrooms_xml_parser.m 실행(main) 소스

우선 showrooms.xml은 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8" ?>
 
<showrooms>
  <showroom>
    <name>강남전시장</name>
    <phone>02-111-1111</phone>
    <dealer>
      <name>김태연</name>
    </dealer>
  </showroom>
  <showroom>
    <name>삼성전시장</name>
    <phone>02-222-2222</phone>
    <dealer>
      <name>구하라</name>
    </dealer>
  </showroom>
</showrooms>

예제에서 showroom 및 dealer 엘리먼트가 각각 name 엘리먼트를 자식으로 가지도록 작성했습니다.

Dealer.h와 Dealer.m은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
#import <Foundation/Foundation.h>
 
 
@interface Dealer : NSObject
{
  NSString* _name;
}
  @property( copy ) NSString* name;
@end
1
2
3
4
5
6
#import "Dealer.h"
 
 
@implementation Dealer
  @synthesize name = _name;
@end

Showroom.h와 Showroom.m은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#import "Dealer.h"
 
 
@interface Showroom : NSObject
{
  NSString* _name;
  NSString* _phone;
  Dealer* _dealer;
}
  @property( copy ) NSString* name;
  @property( copy ) NSString* phone;
  @property( retain ) Dealer* dealer;
@end
1
2
3
4
5
6
7
8
#import "Showroom.h"
 
 
@implementation Showroom
  @synthesize name = _name;
  @synthesize phone = _phone;
  @synthesize dealer = _dealer;
@end

NSXMLParser는 NSXMLDocument의 NSXMLNode처럼 모델이 없기 때문에 분석(parse) 결과를 담을 수 있는 모델이 필요합니다. 이 모델 클래스들은 MVC에도 적용될 수 있기 때문에 모델 클래스를 만들어 두면 조금 더 우아한 프로그래밍할 수 있습니다. 모델 클래스들은 간단하기 때문에 자세한 설명은 생략하도록 하겠습니다.

ShowroomsXmlParser.h는 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#import "Showroom.h"
 
 
@interface ShowroomsXmlParser : NSObject
{
  NSMutableArray* _showrooms;
  NSMutableArray* _elementStack;
}
  - (void) parser: (NSXMLParser*) parser 
      didStartElement: (NSString*) elementName 
      namespaceURI: (NSString*) namespaceURI 
      qualifiedName: (NSString*) qualifiedName 
      attributes: (NSDictionary*) attributeDict;
  - (void) parser: (NSXMLParser*) parser
      foundCharacters: (NSString*) string;
  - (void) parser: (NSXMLParser*) parser 
      didEndElement: (NSString*) elementName
      namespaceURI: (NSString*) namespaceURI 
      qualifiedName: (NSString*) qName;
  - (NSMutableArray*) parseContentOfUrl: (NSURL*) url;
@end

ShowroomsXmlParser는 NSXMLParser가 발생시키는 event를 처리해서 Showroom 객체 배열을 넘겨주는 클래스입니다. NSXMLParser가 위임(delegate)하는 method들을 처리하기 위해서 NSXMLParserDelegate 프로토콜을 구현해야 합니다. 참고로 프로토콜은 자바에서 interface랑 동일합니다. 멤버 변수 _elementStack은 파서의 트리 탐색 경로 및 정보를 담아두는 스택입니다.

ShowroomsXmlParse.m은 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#import "ShowroomsXmlParser.h"
 
 
NSString* const SHOWROOM = @"showroom";
NSString* const DEALER = @"dealer";
NSString* const NAME = @"name";
NSString* const PHONE = @"phone";
 
 
@implementation ShowroomsXmlParser
  - (void) parser: (NSXMLParser*) parser 
      didStartElement: (NSString*) elementName 
      namespaceURI: (NSString*) namespaceURI 
      qualifiedName: (NSString*) qualifiedName 
      attributes: (NSDictionary*) attributeDict
  {
    if ( TRUE == [ elementName isEqualToString: SHOWROOM ] )
    {
      Showroom* showroom = [ Showroom new ];
      [ _elementStack addObject: showroom ];
    }
    else if ( TRUE == [ elementName isEqualToString: DEALER ] )
    {
      Dealer* dealer = [ Dealer new ];
      Showroom* showroom = (Showroom*)[ _elementStack lastObject ];
      [ showroom setDealer: dealer ];
 
      [ _elementStack addObject: dealer ];
    }
    else if ( TRUE == [ elementName isEqualToString: NAME ] ||
      TRUE == [ elementName isEqualToString: PHONE ] )
    {
      NSString* element = [ NSString stringWithString: elementName ];
      [ _elementStack addObject: element ];
    }
  }
 
  - (void) parser: (NSXMLParser*) parser
      foundCharacters: (NSString*) string
  {
    NSCharacterSet* characterSet = [ NSCharacterSet 
      whitespaceAndNewlineCharacterSet ];
    NSString* trimmedValue = [ string stringByTrimmingCharactersInSet:
      characterSet ];
    if ( 0 == [ trimmedValue length ] )
    {
      return;
    }
 
    id element = [ _elementStack lastObject ];
    if ( TRUE == [ element isKindOfClass: [ NSString class ] ] )
    {
      NSUInteger count = [ _elementStack count ];
      NSUInteger index = count - 2;
      id parentElement = [ _elementStack objectAtIndex: index ];
 
      if ( TRUE == [ element isEqualToString: PHONE ] )
      {
        Showroom* showroom = (Showroom*)parentElement;
        [ showroom setPhone: trimmedValue ];
      }
      else if ( TRUE == [ element isEqualToString: NAME ] )
      {
        if ( TRUE == [ parentElement isMemberOfClass: [ Showroom class ] ] )
        {
          Showroom* showroom = (Showroom*)parentElement;
          [ showroom setName: trimmedValue ];
        }
        else if ( TRUE == [ parentElement isMemberOfClass: [ Dealer class ] ] )
        {
          Dealer* dealer = (Dealer*)parentElement;
          [ dealer setName: trimmedValue ];
        }
      }
    }
  }
 
  - (void) parser: (NSXMLParser*) parser 
      didEndElement: (NSString*) elementName
      namespaceURI: (NSString*) namespaceURI 
      qualifiedName: (NSString*) qName
  {
    if ( TRUE == [ elementName isEqualToString: SHOWROOM ] )
    {
      Showroom* showroom = (Showroom*)[ _elementStack lastObject ];
      [ _showrooms addObject: showroom ];
      [ _elementStack removeLastObject ];
    }
    else if ( TRUE == [ elementName isEqualToString: DEALER ] ||
      TRUE == [ elementName isEqualToString: NAME ] ||
      TRUE == [ elementName isEqualToString: PHONE ] )
    {
      [ _elementStack removeLastObject ];
    }
  }
 
  - (NSMutableArray*) parseContentOfUrl: (NSURL*) url
  {
    NSXMLParser* xmlParser = [ [ NSXMLParser alloc ] 
      initWithContentsOfURL: url ];
    [ xmlParser setDelegate: self ];
 
    _showrooms = [ NSMutableArray arrayWithCapacity: 1024 ];
    _elementStack = [ NSMutableArray arrayWithCapacity: 1024 ];
 
    [ xmlParser parse ];
 
    return _showrooms;
  }
@end

NSXMLParser가 동작하기 위해서는 최소한 아래의 메소드들이 구현되어야 합니다.

didStart는 파서가 엘리먼트의 시작 태그를 만났을 때 호출됩니다. 비단말(nonterminal) 노드인 경우 엘리먼트 해당하는 모델 객체를 생성한 후 스택에 넣습니다. 단말(terminal) 노드인 경우 엘리먼트 이름으로 NSString 객체를 만들 후 스택에 넣습니다.

found는 파서가 엘리먼트를 제외한 스트링을 만날을 때 호출됩니다. 인자로 넘어온 스트링의 앞뒤 공백을 제거합니다. 유효한 스트링(이하 속성값)인 경우 스택에서 마지막으로 추가된 NSString 객체를 제거합니다. 제거된 NSString 객체의 값에 따라 적절한 부모 모델 객체의 속성(property)을 속성값으로 설정합니다. 만약 엘리먼트 이름이 NAME인 경우 중복되기 때문에 부모 모델 객체 클래스에 확인합니다.

didEnd는 파서가 엘리먼트의 종료 태그를 만났을 때 호출됩니다. 스택에서 모델 객체를 제거하고 SHOWROOM인 경우 _showrooms 배열 객체에 해당 객체를 추가합니다.

끝으로 콘솔 화면에서 결과를 확인할 수 있는 showrooms_xml_parser.m 파일은 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#import "ShowroomsXmlParser.h"
 
 
int main( int argc, char* argv[] )
{
  NSAutoreleasePool* pool = [ NSAutoreleasePool new ];
 
  ShowroomsXmlParser* xmlParser = [ ShowroomsXmlParser new ];
  NSURL* url = [ NSURL URLWithString: 
    @"http://www.yourserver.com/showrooms.xml" ];
  NSMutableArray* showrooms = [ xmlParser parseContentOfUrl: url ];
 
  for ( Showroom* showroom in showrooms )
  {
    Dealer* dealer = [ showroom dealer ];
    NSLog( @"%@ %@ (%@)", [ showroom name ], [ showroom phone ], 
      [ dealer name ] );
  }
 
  [ xmlParser release ];
  [ url release ];
 
  [ pool drain ];
  return 0;
}

해당 소스들을 컴파일 하기 위해서는 아래의 명령어를 실행합니다.

1
2
$ gcc -framework Foundation Dealer.m Showroom.m ShowroomsXmlParser.m showrooms_xml_parser.m
-o showrooms_xml_parser

실행 결과 화면은 아래와 같습니다.

1
2
3
$ ./showrooms_xml_parser
강남전시장 02-111-1111 (김태연)
삼성전시장 02-222-2222 (구하라)

프로그래밍에 도움되길 바랍니다~

수정 사항

엘리먼트 값이 없는 경우 에러가 발생하는 문제를 해결하기 위해서 showrooms_xml_parser.m의 found 메소드 안에 _elementStack의 마지막 요소 제거 코드를 didEnd로 옮겼습니다.

Post to Twitter Post to Delicious

, , , ,

OS X, HTTP로 XML 데이터 요청 예제

3월 2nd, 2010

오래 간만에 포스팅합니다. 요즘 트위터 때문에 블로그에 소홀해진 것도 있지만 아이폰과 안드로이드 애플케이션(이하, 앱) 개발에 여념이 없습니다. 스마트폰 대세라 흐름을 따라 가는 것도 있지만 주로 웹 서비스만 개발하다 보니 오래 간만에 접하는 애플리케이션 형태의 제품 개발 자체가 매우 흥미롭습니다.

그런데 요즘 왠만한 앱에는 HTTP를 통해서 XML 데이터를 가져와서 보여주는 기능이 들어가는 데 막상 개발 참고하려고 구글 검색해보면 마땅한 예제가 없어서 간단하게 예제를 작성해보았습니다.

요청하는 XML 문서(hello.xml)는 아래와 같이 작성했습니다.

<?xml version="1.0" encoding="UTF-8" ?>
 
<showrooms>
  <showroom><name>강남전시장</name><phone_number>02-111-1111</phone_number></showroom>
  <showroom><name>삼성전시장</name><phone_number>02-222-2222</phone_number></showroom>
  <showroom><name>한남전시장</name><phone_number>02-333-3333</phone_number></showroom>
</showrooms>

해당 XML 데이터를 HTTP를 통해서 가져와서 처리하는 예제는 아래와 같습니다. 간단하게 실행 되는 것을 확인하기 위해서 컨솔 창에서 돌아가도록 작성했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#import <Foundation/Foundation.h>
 
 
int main( int argc, char* argv[] )
{
  NSAutoreleasePool* pool = [ NSAutoreleasePool new ];
  NSURL* url = [ NSURL URLWithString: @"http://www,yourserver.com/hello.xml" ];
  NSError* error;
  NSXMLDocument* xmlDocument = [ [ NSXMLDocument alloc ] 
    initWithContentsOfURL: url
    options: ( NSXMLNodePreserveWhitespace | NSXMLNodePreserveCDATA )
    error: &error ];
 
  if ( nil != error )
  {
    NSLog( @"%@", [ error localizedDescription ] );
    [ pool drain ];
    return 0;
  }
 
  NSXMLNode* rootElement = [ xmlDocument rootElement ];
  NSArray* showrooms = [ rootElement children ];
 
  int i, count = [ showrooms count ];
  for ( i = 0; i < count; ++i ) 
  {
    NSXMLNode* showroomNode = [ showrooms objectAtIndex: i ];
    NSXMLNode* nameNode = [ showroomNode childAtIndex: 0 ];
 
    NSArray* nodes = [ showroomNode nodesForXPath: @"phone_number" error: nil ];
    NSXMLNode* phoneNumberNode = nil;
    if ( 1 == [ nodes count ] )
    {
      phoneNumberNode = [ nodes objectAtIndex: 0 ];
    }
 
    NSLog( @"%@ (%@)", [ nameNode stringValue ], [ phoneNumberNode stringValue ] );
  }
 
  [ xmlDocument release ];
  [ url release ];
 
  [ pool drain ];
  return 0;
}

예제에서 특별히 어려운 내용은 없습니다. NSXMLDocument 클래스가 HTTP를 지원하기 때문에 initWithContentsOfURL 메소드를 호출하기만 하면 됩니다. 그리고 아이폰 런타임 환경에서는 가비지 컬렉션이 지원되지 않기 때문에 NSAutoreleasePool 클래스를 이용해서 메모리 관리를 하였습니다.

예제에서 3가지 다른 방법으로 자식 노드에 접근하고 있습니다. 각각 children, objectAtIndex, nodesForXPath 메소드를 이용합니다. 가장 일반적인 방법은 nodesForXPath을 이용하는 방법입니다. 약간의 성능 문제가 있을 수도 있지만 대부분의 경우 처리하는 XML 데이터의 량이 많이 않기 때문에 신경 쓰지 않아도 될 듯 합니다. 반면 objectAtIndex 메소드를 사용하는 경우 성능은 향상될 수 있겠지만 XML 문법에 종속적이기 때문에 그리 권장할만한 방법은 아닙니다.

해당 파일을 컴파일하는 쉘 구문은 아래와 같습니다.

$ gcc -framework Foundation hello.m -o hello

실행 결과 화면은 아래와 같습니다.

$ ./hello
강남전시장 (02-111-1111)
삼성전시장 (02-222-2222)
한남전시장 (02-333-3333)

프로그래밍에 도움되길 바랍니다~

[ 수정 사항 ]
조사해 본 결과 NSXMLDocument 클래스는 iPhone SDK에 제외되어 있습니다. 따라서 위 예제는 OS X 애플리케이션에서만 동작합니다. 빠른 시일 내에 iPhone에서 돌아가는 예제를 작성해서 올리도록 하겠습니다. iPhone은 iPhone, HTTP로 XML 데이터 요청 및 처리 예제를 참고하세요~

Post to Twitter Post to Delicious

, , , ,