HTML 파싱할 일이 생겼는데,
그동안은 그냥그냥 필요한 내용만 crummy에서 짬짬히 보다가,
BeautifulSoup을 한국말로 잘 정리한 사이트를 찾았다.

susukang98님의 블로그 : http://susukang98.springnote.com/pages/333771

예를 들자면, BeautifulSoup을 이용해서 특정 홈피의 내용 중, 어느 부분은 읽는다면 다음과 같이 간단하게 끝낼수 있을 것이다.
(사실 정규식을 잘 쓴다면 필요없을 것이다...)

from BeautifulSoup import BeautifulSoup
import urllib2

url = 'http://블라블라'
handle = urllib2.urlopen(url)
data = handle.read()
soup = BeautifulSoup(data)
article = str( soup('div', {'class':'article',}) ) #div내의 article class 추출
print article.decode('utf8')


위 예제 프로그램은 본문에서 div 내의 article 클래스만을 추출하는 예제이다. (ex. 티스토리)


자세한 내용은 다시 찾기 귀차니즘으로 인해, 수수깡님이 스크랩한 내용을 아래에 copy&paste 해 놓겠다.

-----------------------------------------------------------------------------------

Beautiful Soup (2.1.1)

http://www.crummy.com/software/BeautifulSoup/



웹을 가지고 놀기 위해서는 먼저 웹의 언어인 HTML을 잘 구사할 수 있어야 한다.

세상에는 프로그래머가 HTML을 잘 말하고 잘 알아듣기 위해 사용하는 HTML 파서가

무수히 많다.  그중에서 사용하기 쉬운 파서를 하나 고르자면 Beautiful Soup을 들

수 있다.  Beautiful Soup은 파이선으로 작성되었으며, 동적 스크립트언어의 장점을

잘 활용한다.

#import urllib

#html_source = urllib.urlopen('http://www.naver.com').read()

html_source = '''
<html>
<head><title>페이지 제목</title></head>
<body>
<p class="layout"><b id="key">첫번재 단락</b></p>
<p>
<b>두번째 <i>단락</b> 시작</i>
<hr>
<ul>
    <li><img src="dooly-1.png" name="main" height=100 width=70>둘리
    <li><img src="dooly-2.png" height="70" width="50">도우너
    <li><img src="dooly-3.png" height="30" width="20">또치
    <li><img src="dooly-4.png">마이콜
</ul>
</p>

<p>
</body>
</html>'''



from BeautifulSoup import BeautifulSoup

soup = BeautifulSoup(html_source)



# 테그 이름을 변수 이름으로 사용할 수 있다.
print soup.html.head.title
# 결과: <title>페이지 제목</title>



# 계층구조의 중간단계를 생략할 수 있다.
print soup.title
# 결과: <title>페이지 제목</title>



# 테그 안에 다른 테그가 없는 경우 string 속성으로 테그 내용을 얻을 수 있다.
print soup.title.string
# 결과: 페이지 제목



# 같은 이름의 테그가 여러개 있다면 제일 먼저 나오는 테그를 알려준다.
# dictionary 문법을 사용하여 테그의 속성만 얻을 수도 있다.
print soup.p
# 결과: <p class="layout"><b>첫번째 단락</b></p>
print soup.p['class']
# 결과: layout



# 없는 테그를 지칭하면 (BeautifulSoup.) Null 객체를 반환한다.
print soup.body.title
# 결과: Null



# soup('p') 은 첫번째 뿐아니라 모든 p 테그 목록을 반환한다.
# 두번째 아규먼트로 테그의 속성을 제한할 수도 있다.
print soup('p')[0]
# 결과: <p class="layout"><b>첫번째 단락</b></p>
print soup('img', { 'name': 'main', })
# 결과: [<img src="dooly-1.png" name="main" height="100" width="70" />]
print soup('p', 'layout')
# soup('p', { 'class': 'layout' }) 과 같다. CSS 분류를 쉽게 지정할 수 있다.



# parent 속성은 계층구조상 한칸 위에 있는 테그를 지칭하고, 반대로 contents

# 속성은 계층구조상 한칸 아래에 있는 테그 목록을 반환한다.
# nextSibling 와 previousSibling 은 계층구조상 같은 위치에 있는 바로 앞뒤 테그를

# 지칭한다.  예제에서 첫번째 p 테그의 nextSibling 은 두번째 p 테그가 아니라

# 첫번째 p 테그와 두번째 p 테그 사이 영역이고, 이 영역에는 줄바꿈 문자 하나만 있다.
# 이는 soup('p').parent.contents 로 확인할 수 있다.
# next 와 previous 는 계층구조와 무관하게 HTML 소스에서 테그 바로 앞뒤에 위치하는

# 테그를 지칭한다.  마지막으로 테그 이름은 name 속성에 저장된다.
print soup('p')[0].nextSibling
# 결과: \n
print soup('p')[0].next
# 결과: <b>첫번째 단락</b>

print soup('p')[0].next.name
# 결과: b
# 앞에서 본 string 속성은 테그 안에 다른 테그가 없는 경우에는 contents[0] 과 같고,
# 다른 테그가 있다면 Null 이다.



print len(soup('p')) # len(soup('p').contents) 와 같다.
for x in soup('p'): # for x in soup('p').contents: 와 같다.

    pass



## fetch(name, attrs, recursive, limit) 함수
# 다양한 조건을 가지고 원하는 테그를 찾는 함수로, 앞의 예들은 이 함수의 축약형이다.

#       tag.fetch(...) = tag(...)
# name과 attrs는 각각 테그 이름과 테그 속성을 나타내는데 다양한 방법으로 지시할 수 있다.

#
# * 문자열: fetch('img'), 모든 img 테그 목록
# * 목록: fetch(['object', 'applet', 'embed']), 모든 object/applet/embed 테그 목록
# * dictionary: fetch('div', { 'class': 'sidebar', 'name': 'menu' })
# * 정규표현식: fetch('div', { 'name': re.compile('list.*') }),

#                                           name 속성이 "list"로 시작하는 모든 div 테그 목록
# * 함수: 원하는 조건인 경우 참을 반환하는 함수를 사용하여 복잡한 조건을 지시할 수 있다.

#
# recursive와 limit는 계층구조상 현재 테그 아래를 계속 찾아들어갈지, 만약 그렇다면

# 어느정도까지 들어갈지를 정한다.  기본적으로 현재 테그 아래로 끝까지 들어가면서

# 테그를 찾는다.

#
# fetch() 를 기준삼아 first(), fetchText(), firstText(), findNextSibling(),

# findPreviousSibling(), fetchNextSibling(), fetchPreviousSibling(),
# findNext(), findPrevious(), fetchNext(), fetchPrevious(), findParent(),
# fetchParent() 와 같은 함수가 있다.  fetch*/*Text() 함수는 테그가 아닌 테그 안의

# 문자를 찾거나 가져오고, *Next*/*Previous*/*Parent() 함수는 현재 테그에서

# 계층구조상 아래로 내려가지 않고 대신 앞뒤 혹은 위로 이동하며 조건에 맞는 테그를

# 찾는다.  각 함수의 자세한 정보는 설명서를 참고하라.
def need_thumbnail(x):
    # 가로나 세로가 60 보다 큰 img 테그라면 True, 아니면 False

    if x.name == 'img':

        return x.get('height', 0) > 60 or x.get('width', 0) > 60

    return False
print soup.ul(need_thumbnail) # = soup.ul.fetch(need_thumbnail)
print soup.p.findNextSibling('p') # 두번째 p 테그



# 다음과 같이 HTML 소스를 수정할 수도 있다.  단, 이때는 앞에서 본 string 같은
# 축약형을 사용할 수 없고 contents 목록을 직접 수정해야 한다.  그후 prettify()

# 함수로 수정한 HTML 소스를 계층구조에 따라 들여쓰기하여 출력한다.
print soup
soup.title.contents[0] = '제목 수정'
soup.p['class'] = 'menu'
soup('p')[1].contents = ['두번째 단락 생략',]

del soup.body.contents[5]
print soup.prettify()

현재 Beautiful Soup은 두가지 문제가 있는데, 하나는 속도이고 다른 하나는 한글처리다.

Beautiful Soup은 빠른 속도를 위해 최적화하여 설계되지 않았기때문에 복잡한 HTML

소스를 처리할 때 속도가 느려진다.  이런 경우에는 자주 참조하는 테그의 공통분모를

미리 변수에 저장해두고 이 변수를 기준으로 테그들을 참조하는 식으로 부담을 덜 수 있다.



    soup('div', { 'name': 'toolbar' })[0].table('tr')[0]('td')[2].ul('li')[0]

    soup('div', { 'name': 'toolbar' })[0].table('tr')[0]('td')[2].ul('li')[1]

    soup('div', { 'name': 'toolbar' })[0].table('tr')[0]('td')[2].ul('li')[2]

    soup('div', { 'name': 'toolbar' })[0].table('tr')[0]('td')[2].ul('li')[3]

->

    ulist = soup('div', { 'name': 'toolbar' })[0].table('tr')[0]('td')[2].ul

    ulist('li')[0]

    ulist('li')[1]

    ulist('li')[2]

    ulist('li')[3]



한글처리에서는 테그의 속성값에 한글이 있는 경우 테그 속성값을 전부 무시해 버린다.
정확히는 파이선 표준 라이브러리의 sgmllib의 문제인데, BeautifulSoup.py 파일의
    from sgmllib import SGMLParser, SGMLParseError
줄을
    from hack_sgmllib import SGMLParser, SGMLParseError
으로 수정하고, 표준 라이브러리의 sgmllib.py 파일의 복사본을 BeautifulSoup.py 와

동일한 디렉토리에 hack_sgmllib.py 란 이름으로 저장한다.  그리고 attrfind 정규표현식에서

[-a-zA-Z0-9./,:;+*%?!&$\(\)_#=~\'"@] 부분을 [^ >] 로 수정한다. 깔끔한 방법은

아니지만 어쨌든 한글 테그 속성을 인식하게 된다.
AND