어제 클리앙 모두의 공원 게시판은 NSString 관련 메소드만 가지고 파싱을 해 보았는데요. 코드도 꽤 길어지고 굉장히 무식한 방법이었습니다. 어제 포스팅 마지막에 언급했던대로 좀 더 편한 방법들을 살펴보려고 하는데요. hpple라는 API가 있습니다. 나온지도 오래되었고(업데이트 된지도...), 제가 원래 외부API를 잘 사용안하는데요. 뭐 대체제가 없다보니 써 보겠습니다.
일단 아래 주소에서 받으실 수 있구요.
https://github.com/topfunky/hpple
프로젝트에 6개의 파일을 포함시켜 주시면 됩니다.
TFHpple.h
TFHpple.m
TFHppleElement.h
TFHppleElement.m
XPathQuery.h
XPathQuery.m
그리고 환경설정을 두개 해 주셔야 하는데 PROJECT환경설정에서(Build Settings) All을 누르시면 여러가지 설정들을 찾아보실 수 있는데요. 설정해야될 것은 아래와 같습니다.
Header Search Paths : ${SDKROOT}/usr/include/libxml2
Other Linker Flags : -lxml2
그리고 마지막으로 html파싱이 이루어지는 프로젝트에 TFHpple.h를 import 시켜주시면 설정은 다 끝났습니다.
프로젝트에 포함시킨 소스파일들을 한번 살펴보면 실제 파싱은 XPathQuery에서 이루어 지고 있음을 확인할 수 있습니다. 다른 소스들도 복잡하지 않으니 한번 살펴보시면 좋을것 같고요.
어제 클리앙 모공을 파싱했던 부분 기억나시나요. 가져온 데이터가 게시물제목, 링크, 아이디, 조회수, 글쓴시간이었는데요. 실제 데이터 파싱하는 부분만 구현 해 보겠습니다.
// 클리앙 모공
NSString * path = @"";
NSError * error;
NSString * stringFromURL = [[NSString alloc] initWithContentsOfURL:[NSURLURLWithString:@"http://clien.career.co.kr/cs2/bbs/board.php?bo_table=park"] encoding:NSUTF8StringEncodingerror:&error];
if(stringFromURL == nil)
{
NSLog(@"Error reading URL at %@\n%@", path, [error localizedFailureReason]);
}
윗 부분은 어제 했던 부분과 동일하고요.
NSData * data = [stringFromURL dataUsingEncoding:NSUnicodeStringEncoding];
// Create parser
TFHpple * xpathParser = [[TFHpple alloc] initWithHTMLData:data];
// 타이틀 & URL
NSArray * elements = [xpathParser searchWithXPathQuery:@"//tr//td[2]//a"];
for(int i = 0 ; i < [elements count] ; i++)
{
NSLog(@"%@", [[elements objectAtIndex:i] content]); // 타이틀
NSLog(@"%@", [[[elements objectAtIndex:i] attributes] valueForKey:@"href"]); // URL
}
타이틀과 URL을 파싱해서 가져오는 방법입니다. 오렌지색으로 하이라이트 된 부분과 일치하는 형태를 찾아서 elements에 배열형태로 넣는 방법인데요. tr태그 내부에 td태그가 2번 나온 뒤 a태그가 나오는 형태를 찾으라는 말인데요.
<tr class="mytr">
<td>10816621</td>
<td class="post_subject"> <a href='../bbs/board.php?bo_table=park&wr_id=10816621' >[차단공시]삭제요청에 의한 차단공시</a></td>
<td class="post_name"><img src='../data/member/ci/cipher.gif' width='54' height='16' align='absmiddle' border='0'></td>
<td><span title="2012-02-15 17:25:24">17:25</a></td>
<td>1</td>
</tr>
<tr class="mytr">
<td>10816618</td>
<td class="post_subject"> <a href='../bbs/board.php?bo_table=park&wr_id=10816618' >초고도 근시인데 라섹했습니다</a></td>
<td class="post_name"><img src='../data/member/hi/hikari.gif' width='60' height='16' align='absmiddle' border='0'></td>
<td><span title="2012-02-15 17:25:11">17:25</a></td>
<td>10</td>
</tr>
<tr class="mytr">
<td>10816613</td>
<td class="post_subject"> <a href='../bbs/board.php?bo_table=park&wr_id=10816613' >암스텔담에선 자전거 뒤에 타려면 이 정도 운동 신경은 가져야...</a></td>
<td class="post_name"><span class='member'>Guylian</span></td>
<td><span title="2012-02-15 17:25:01">17:25</a></td>
<td>36</td>
</tr>
위의 html 중 색칠해진 부분들이 배열에 들어가게 됩니다. 여기서 한가지 보셔야 할 것은..
content의 경우 해당 태그가 열리고 닫힌 사이의 값, 즉 아래에서 색칠된 부분입니다.
<tr class="mytr">
<td>10816618</td>
<td class="post_subject"> <a href='../bbs/board.php?bo_table=park&wr_id=10816618' >초고도 근시인데 라섹했습니다</a>
<a 태그가 열리고, </a>로 닫힌 사이의 값입니다.
attribute의 경우 해당 태그 내부에서 key=value 형태로 저장된 값입니다. NSDictionary형의 값이기 때문에 태그의 key가 딕셔너리의 키가 되고 태그의 value가 딕셔너리의 value가 됩니다.
<tr class="mytr">
<td>10816618</td>
<td class="post_subject"> <a href='../bbs/board.php?bo_table=park&wr_id=10816618' >초고도 근시인데 라섹했습니다</a>
여기에서는 오렌지색이 key가 되고 청록색이 value가 되기 때문에
[[[elements objectAtIndex:i] attributes] valueForKey:@"href"]
이런식으로 value를 가져올 수 있었습니다.
그럼 아이디를 가져오는 방법을 살펴볼텐데요. 아이디의 경우 두가지 형태가 있기 때문에 조금 복잡합니다. 일단 html 로 아이디가 스트링인 경우와 아이디가 이미지인 경우를 살펴보면요.
<tr class="mytr">
<td>10816618</td>
<td class="post_subject"> <a href='../bbs/board.php?bo_table=park&wr_id=10816618' >초고도 근시인데 라섹했습니다</a></td>
<td class="post_name"><img src='../data/member/hi/hikari.gif' width='60' height='16' align='absmiddle' border='0'></td>
<td><span title="2012-02-15 17:25:11">17:25</a></td>
<td>10</td>
</tr>
<tr class="mytr">
<td>10816613</td>
<td class="post_subject"> <a href='../bbs/board.php?bo_table=park&wr_id=10816613' >암스텔담에선 자전거 뒤에 타려면 이 정도 운동 신경은 가져야...</a></td>
<td class="post_name"><span class='member'>Guylian</span></td>
<td><span title="2012-02-15 17:25:01">17:25</a></td>
<td>36</td>
</tr>
3번째 td의 다음 element가 달라지는 것을 알 수 있는데요. 몇가지 방법이 떠오르긴 합니다만 직관적으로 스트링을 가져와서 nil이면 이미지로 판단해서 처리하는 방법으로 진행하겠습니다.
// 아이디
elements = [xpathParser searchWithXPathQuery:@"//tr//td[3]"];
for (int i = 0; i < [elements count]; i++)
{
if(((NSString *)[[[[elements objectAtIndex:i] children] objectAtIndex:0]content]) != nil)
{
// 아이디가 스트링이면 스트링 출력
NSLog(@"%@" , ((NSString *)[[[[elements objectAtIndex:i] children] objectAtIndex:0] content]) ) ;
}else
{
// 아이디가 이미지이면 링크 출력
NSLog(@"%@", ((NSString *)[[[[[elements objectAtIndex:i] children] objectAtIndex:0] attributes]valueForKey:@"src" ]));
}
}
위에서 한 방법과 똑같은 방법이기 때문에 따로 상세설명은 안하겠습니다. 이 코드 위쪽에 첨부된 html부분과 잘 비교해 보세요.
뭐 여기까지 왔다면 글 작성시간과 조회수는 쉽습니다.
// 글작성시간
elements = [xpathParser searchWithXPathQuery:@"//tr//td[4]//span"];
NSLog(@"%@", [[elements objectAtIndex:0] content]);
// 조회수
elements = [xpathParser searchWithXPathQuery:@"//tr//td[5]"];
NSLog(@"%@", [[elements objectAtIndex:0] content]);
이렇게 필요한 정보를 다 가져올 수 있었습니다. 다음번에는 정규식을 이용한 파싱을 해 보겠습니다.
Stack overflow에서 보니 절대 html파싱은 정규식으로 하지 말라는 말이 많이 보입니다. 정규식이 다 좋은데 익숙해지지 않으면 읽기가 쉽지 않기때문에 그런데요. 사실 저도 정규식에는 근본없는 배움인지라...