Tag Archives: libxml2

jQuery, Jaxer, HTML Parsing

jQuery를 본격적(?)으로 쓰기 시작했는데 쓸수록 마음에 드네요. 자바스크립트 답지 않은 문법, selector를 통한 자유로운 element 선택, 한번에 여러 element에 작업할수 있는 특징. 어떻게 이런 문법으로 라이브러리를 만들 생각을 했는지, 그리고 이렇게 잘 구현했는지 여러가지 생각을 하게끔하는 라이브러리입니다.

웹프로그래밍을 하면 MVC 분리가 잘 안됐었는데 jQuery를 이용하면 이게 어느정도 가능한거 같네요. 일주일동안 사용하는데 너무 마음에 들어서 서버쪽에서도 이런 작업을 하면 편할거 같은 생각이 들어서 좀 찾아봤습니다.

먼저 자바로 짠 자바스크립트 엔진인 Rhino 위에서 여러가지 자바스크립트 라이브러리(jquery, prototype, mochkit)를 돌린 시도가 있어서 저도 따라해봤지만 뭐가 문제인지 정상동작하지 않더군요. 예제에 보면 자바스크립트로 서버쪽 스크립팅을 하는데, 일단 화면에 출력하는거부터 익숙하지가 않으니, 뭐가 잘못됐는지 찾아보기도 힘들더군요. 글 올라온지 1년이 넘어서 버전 차이때문에 그런건지… 자바의 버전때문에 그런건지… 하여간 중간에 포기!

문제 해결을 위해서 웹서핑하다가 Aptana Jaxer에 대해서 알게되었습니다. 전에 이름만 들어봤었는데, 자바스크립트로 서버단에서도 프로그래밍할수 있게 하는 프로젝트입니다. 단순히 HTML에서 script에 runat=”server”로 달아주면, 해당 부분이 서버단에서 처리한 다음에 결과가 클라이언트에 전달됩니다. 클라이언트 단에서 잘 돌아가는 자바스크립트를 서버단에서 수정없이 바로 돌릴수 있습니다. jQuery등의 라이브러리도 서버단에서 잘 돌아갑니다:) 여기서 더 나아가 서버와 클라이언트 자바스크립트간 통신을 할수 있는 방법을 만든거 같네요. 서버단 자바스크립트에서는 디비접속, 파일접근 등 서버단 언어에서 할수 있는 기능들을 라이브러리로 제공합니다. 간단히 클라이언트 단에서 하던 일 정도는 간편하게 서버단에서 처리할수 있지만, 웹서버도 따로 띄워야하고, 다른 서버단 언어와의 연동을 생각하니, 서버단에서 jQuery를 돌린 결과값만 HTML로 받아서 저장하는게 더 편할것 같다는 생각이 들어서 Jaxer 도입은 보류했습니다.

jQuery처럼 편하게 HTML 데이타에 접근해서 데이타를 추출하고, 변경할수 있는 방법을 찾아봤습니다. jQuery의 selector나 XPath등으로 HTML을 파싱할수 있으면 좋을거라 생각하고 검색해보니 libxml2에서 HTML 파싱을 지원하더군요:) python binding이 있고, 그전에 XML에서는 XPath를 이용한 경험도 있으니 금방 될듯하더군요. jQuery로 짠 라인들을 복사하고, 그대로 XPath와 libxml2 라이브러리 함수들로 옮겼습니다. 라인수는 많이 길어졌지만 그래도 소스는 볼만하고, selector로는 불가능한 부분들도 쉽게 구현할수 있다는 생각이 들더군요.

[code type=javascript]
   $(‘a[href^=/]’).each(function(i) {$(this).replaceWith($(this).text()); } );
   $(‘a[href^=#cite]’).remove();
   $(‘span[class=editsection]’).remove();
   $(‘table[class^=navbox ]’).remove();
   $(‘div[class=주석]’).remove();
   $(‘table[id=toc]’).remove();
   $(‘h2 > span’).each(function(i) { if ($(this).text() == “주석”) $(this).remove(); } );
[/code]
[code type=python]
   try:
       parse_options = libxml2.HTML_PARSE_RECOVER + libxml2.HTML_PARSE_NOERROR + libxml2.HTML_PARSE_NOWARNING
       hdoc = libxml2.htmlReadFile(filename, None, parse_options)
   except Exception, e:
       print e
       return

   #$(‘a[href^=/]’).each(function(i) {$(this).replaceWith($(this).text()); } );
   #$(‘a[href^=#cite]’).remove();
   for e in hdoc.xpathEval(‘//a’):
       href = e.prop(“href”)
       if not href: continue
       if href.find(“/”)==0:
           node = libxml2.newText(e.content)
           e.replaceNode(node)
       elif href.find(“#cite”)==0:
           e.unlinkNode()

   #$(‘span[class=editsection]’).remove();
   for e in hdoc.xpathEval(‘//span[@class=”editsection”]’): e.unlinkNode()

   #$(‘table[class^=navbox ]’).remove();
   for e in hdoc.xpathEval(‘//table’):
       cl = e.prop(“class”)
       if not cl: continue
       if cl.split(” “).count(“navbox”)>0:
           e.unlinkNode()

   #$(‘div[class=주석]’).remove();
   for e in hdoc.xpathEval(‘//div[@class=”주석”]’): e.unlinkNode()

   #$(‘table[id=toc]’).remove();
   for e in hdoc.xpathEval(‘//table[@id=”toc”]’): e.unlinkNode()

   #$(‘h2 > span’).each(function(i) { if ($(this).text() == “주석”) $(this).remove(); } );
   for e in hdoc.xpathEval(‘//h2/span’):
       if e.content == “주석”:
           e.unlinkNode()
[/code]

libxml2 python binding의 문제인지 모르겠지만, htmlParseDoc()을 사용하면 xpath가 동작하지 않고, htmlParseFile을 사용하면 html 파싱 관련 경고가 출력되는데 출력안되게 할 방법이 없는듯하네요.

위 소스는 위키피디아에서 글을 읽어서 필요없는 부분을 제거하는 소스이고, 이후로 여러가지 요구사항들이 생겨서 많이 확장됐습니다.

XPath & libxml2

XML은 꽤 오래전부터 사용했지만, 여러가지 복잡한 용어들이 나오면서 좀 멀어졌던 느낌이었는데, XPath는 정말 프로그래머에게 유용한 툴인것 같네요.

XPath는 XML 문서에서 쉽게 element를 찾는 API로 쿼리를 문자열로 넘기면 조건에 맞는 element나 element 리스트를 반환하게 됩니다. 1.0 버전이 있고 2.0 버전이 최근에 나왔습니다. 아직까지는 라이브러리들이 1.0 기반이 대부분입니다.

쿼리는 예제로 살펴보는것이 빠른듯하네요.

“A/B/C” : A element 밑에 B element 밑에 C element들은 찾을때
“/A/B/C” : 위와 같지만 A가 최상위 element.
“/A/B/C[1]” : C element중 첫번째
“/A/B/C[2]” : C element중 두번째
“//C” : 모든 C element
“B//C” : B 하위에 있는 C element
“A/B/*” : A element 밑에 B element 바로 밑의 모든 element
“A/B//*” : A element 밑에 B element 밑의 모든 element (하위 element 포함)
“//*” : 문서의 모든 element

libxml2라는 C 라이브러리가 있지만 python wrapper를 이용하여 python에서 위의 쿼리들을 돌려봤습니다. 테스트해본 결과 /로 시작하지 않는 쿼리들은 제대로 동작하지 않더군요. (검색되는 결과가 없음) 이런 쿼리들은 앞에 //를 붙여주면 제대로 동작합니다.

아래는 python 소스입니다. 중간에 예제 XML을 보기 좋게(?) 들여쓰기했놨지만 출력할때 한줄로 볼수 있도록 xml에서 공백과 newline을 제거합니다.

[CODE type=python]
import libxml2

def xpathElements(ctxt, query):
   if query[0] == ‘/':
       print “\”%s\”” % query
   else:
       print “\”%s\” -> \”//%s\”” % (query, query)
       query = “//” + query
   res = ctxt.xpathEval(query)
   for e in res:
       print ”    %s (%s)” % (e.name, e)

xml = “””
<A>
   <B>
       <C id=’c1’/>
       <C id=’c2′>
           <D/>
       </C>
       <E>
           <F/>
           <A>
               <B>
                   <C/>
               </B>
           </A>
       </E>
   </B>
</A>”””

xml = ”.join([l.strip() for l in xml.splitlines()])

doc = libxml2.parseDoc(xml)

ctxt = doc.xpathNewContext()

xpathElements(ctxt, “A/B/C”)
xpathElements(ctxt, “/A/B/C”)
xpathElements(ctxt, “/A/B/C[1]”)
xpathElements(ctxt, “/A/B/C[2]”)
xpathElements(ctxt, “/A/B/C[3]”)
xpathElements(ctxt, “//C”)
xpathElements(ctxt, “//B//C”)
xpathElements(ctxt, “A/B/*”)
xpathElements(ctxt, “A/B//*”)
xpathElements(ctxt, “//*”)
[/HTML][/CODE]

다음은 실행결과입니다.

“A/B/C” -> “//A/B/C”
   C (<C id=”c1″/>)
   C (<C id=”c2″><D/></C>)
   C (<C/>)
“/A/B/C”
   C (<C id=”c1″/>)
   C (<C id=”c2″><D/></C>)
“/A/B/C[1]”
   C (<C id=”c1″/>)
“/A/B/C[2]”
   C (<C id=”c2″><D/></C>)
“/A/B/C[3]”
“//C”
   C (<C id=”c1″/>)
   C (<C id=”c2″><D/></C>)
   C (<C/>)
“//B//C”
   C (<C id=”c1″/>)
   C (<C id=”c2″><D/></C>)
   C (<C/>)
“A/B/*” -> “//A/B/*”
   C (<C id=”c1″/>)
   C (<C id=”c2″><D/></C>)
   E (<E><F/><A><B><C/></B></A></E>)
   C (<C/>)
“A/B//*” -> “//A/B//*”
   C (<C id=”c1″/>)
   C (<C id=”c2″><D/></C>)
   D (<D/>)
   E (<E><F/><A><B><C/></B></A></E>)
   F (<F/>)
   A (<A><B><C/></B></A>)
   B (<B><C/></B>)
   C (<C/>)
“//*”
   A (<A><B><C id=”c1″/><C id=”c2″><D/></C><E><F/><A><B><C/></B></A></E></B></A>)
   B (<B><C id=”c1″/><C id=”c2″><D/></C><E><F/><A><B><C/></B></A></E></B>)
   C (<C id=”c1″/>)
   C (<C id=”c2″><D/></C>)
   D (<D/>)
   E (<E><F/><A><B><C/></B></A></E>)
   F (<F/>)
   A (<A><B><C/></B></A>)
   B (<B><C/></B>)
   C (<C/>)