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 = '''
'''
from BeautifulSoup import BeautifulSoup
soup =
BeautifulSoup(html_source)
# 태그 이름을 변수 이름으로 사용할 수 있다.
print
soup.html.head.title
# 결과:
# 계층구조의 중간단계를 생략할 수 있다.
print
soup.title
# 결과:
# 태그 안에 다른 태그가 없는 경우 string 속성으로 태그 내용을 얻을 수
있다.
print soup.title.string
# 결과: 페이지 제목
# 같은 이름의 태그가 여러개
있다면 제일 먼저 나오는 태그를 알려준다.
# dictionary 문법을 사용하여 태그의 속성만 얻을 수도 있다.
print
soup.p
# 결과:
첫번째 단락
print soup.p['class']
# 결과:
layout
# 없는 태그를 지칭하면 (BeautifulSoup.) Null 객체를 반환한다.
print
soup.body.title
# 결과: Null
# soup('p') 은 첫번째 뿐아니라 모든 p 태그 목록을
반환한다.
# 두번째 아규먼트로 태그의 속성을 제한할 수도 있다.
print soup('p')[0]
# 결과:
첫번째 단락
print soup('img', { 'name': 'main', })
#
결과: []
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
# 결과: 첫번째 단락
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./,:;+*%?!&$\(\)_#=~\'"@] 부분을 [^ >] 로 수정한다. 깔끔한
방법은
아니지만 어쨌든 한글 태그 속성을 인식하게 된다.