뷰티플 수프 문서

작성 레오날드 리차드슨(Leonard Richardson) (leonardr@segfault.org)

한글판 johnsonj 2008.03.29

뷰티플수프는 파이썬용 HTML/XML 해석기로서 무효한 조판조차도 해석 트리로 바꾸어 준다. 상용적인 방법으로 간단하게 해석 트리를 항해하고 탐색하며 수정할 수 있다. 일반적으로 프로그래머의 수고와 시간을 덜어준다. Rubyful Soup이라는 이름으로 루비로도 이식되어 있다.

이 문서는 뷰티플수프 버전 3.0을 예제와 더불어 주요한 모든 특징을 보여준다. 무엇에 사용할 수 있는지, 어떻게 작동하는지, 사용하는 방법, 원하는 것을 하도록 만드는 법, 그리고 예상을 벗어날 때 어떻게 대처해야 하는지 보여준다.

목차

시작하기

뷰티플수프는 여기에서 내려받자. 변경 이력에 3.0과 이전 버전 사이의 차이점을 기술해 두었다.

다음과 같은 한 줄을 어플리케이션에 두어 뷰티플수프를 삽입하자:

from BeautifulSoup import BeautifulSoup          # HTML 처리용
from BeautifulSoup import BeautifulStoneSoup     # XML 처리용
import BeautifulSoup                             # 이것저것 모두

다음 코드는 뷰티플수프의 기본적인 특징을 보여준다. 이 코드를 파이썬 세션에 붙여 넣고 직접 실행해 보시면 된다.

from BeautifulSoup import BeautifulSoup
import re

doc = ['<html><head><title>Page title</title></head>',
       '<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',
       '<p id="secondpara" align="blah">This is paragraph <b>two</b>.',
       '</html>']
soup = BeautifulSoup(''.join(doc))

print soup.prettify()
# <html>
#  <head>
#   <title>
#    Page title
#   </title>
#  </head>
#  <body>
#   <p id="firstpara" align="center">
#    This is paragraph
#    <b>
#     one
#    </b>
#    .
#   </p>
#   <p id="secondpara" align="blah">
#    This is paragraph
#    <b>
#     two
#    </b>
#    .
#   </p>
#  </body>
# </html>

다음은 수프를 항해하는 몇가지 방법이다:

soup.contents[0].name
# u'html'

soup.contents[0].contents[0].name
# u'head'

head = soup.contents[0].contents[0]
head.parent.name
# u'html'

head.next
# <title>Page title</title>

head.nextSibling.name
# u'body'

head.nextSibling.contents[0]
# <p id="firstpara" align="center">This is paragraph <b>one</b>.</p>

head.nextSibling.contents[0].nextSibling
# <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>

다음은 특정 태그, 또는 특정 특성을 가진 태그에 대하여 수프를 검색하는 두 가지 방법이다:

titleTag = soup.html.head.title
titleTag
# <title>Page title</title>

titleTag.string
# u'Page title'

len(soup('p'))
# 2

soup.findAll('p', align="center")
# [<p id="firstpara" align="center">This is paragraph <b>one</b>. </p>]

soup.find('p', align="center")
# <p id="firstpara" align="center">This is paragraph <b>one</b>. </p>

soup('p', align="center")[0]['id']
# u'firstpara'

soup.find('p', align=re.compile('^b.*'))['id']
# u'secondpara'

soup.find('p').b.string
# u'one'

soup('p')[1].b.string
# u'two'

수프를 수정하는 것은 아주 쉽다:

titleTag['id'] = 'theTitle'
titleTag.contents[0].replaceWith("New title")
soup.html.head
# <head><title id="theTitle">New title</title></head>

soup.p.extract()
soup.prettify()
# <html>
#  <head>
#   <title id="theTitle">
#    New title
#   </title>
#  </head>
#  <body>
#   <p id="secondpara" align="blah">
#    This is paragraph
#    <b>
#     two
#    </b>
#    .
#   </p>
#  </body>
# </html>

soup.p.replaceWith(soup.b)
# <html>
#  <head>
#   <title id="theTitle">
#    New title
#   </title>
#  </head>
#  <body>
#   <b>
#    two
#   </b>
#  </body>
# </html>

soup.body.insert(0, "This page used to have ")
soup.body.insert(2, " &lt;p&gt; tags!")
soup.body
# <body>This page used to have <b>two</b> &lt;p&gt; tags!</body>

다음은 실제-세계의 예이다. ICC Commercial Crime Services 주간 프라이버시 보고서를 가져와, 뷰티플수프로 해석한 다음, 사생활 침해 사건을 추출한다:

import urllib2
from BeautifulSoup import BeautifulSoup

page = urllib2.urlopen("http://www.icc-ccs.org/prc/piracyreport.php")
soup = BeautifulSoup(page)
for incident in soup('td', width="90%"):
    where, linebreak, what = incident.contents[:3]
    print where.strip()
    print what.strip()
    print

문서 해석하기

뷰티플수프 구성자는 XML이나 HTML 문서를 문자열의 형태 (또는 열린 파일-류 객체)로 취한다. 그 문서를 해석하여 그에 상응하는 데이터 구조를 메모리에 만든다.

뷰티플수프가 완벽하게 모양을 갖춘 문서를 돌려주면, 해석된 데이터 구조는 원래 문서와 똑 같이 보인다. 그러나 문서에 무엇인가 문제가 있으면, 뷰티플수프는 머리를 써서(heuristics) 그 데이터 구조에 대하여 적당한 구조를 추측한다.

HTML 해석하기

BeautifulSoup 클래스를 사용하여 HTML 문서를 해석하자. 다음은 BeautifulSoup가 이해하는 것들이다:

다음은 작동하는 예이다:

from BeautifulSoup import BeautifulSoup
html = "<html><p>Para 1<p>Para 2<blockquote>Quote 1<blockquote>Quote 2"
soup = BeautifulSoup(html)
print soup.prettify()
# <html>
#  <p>
#   Para 1
#  </p>
#  <p>
#   Para 2
#   <blockquote>
#    Quote 1
#    <blockquote>
#     Quote 2
#    </blockquote>
#   </blockquote>
#  </p>
# </html>

BeautifulSoup가 합리적인 위치를 가늠하여 닫기 태그를 배치했다는 것에 주목하자. 심지어 원래 문서에 없더라도 말이다.

이 문서는 유효한 HTML이 아니지만, 그렇게 나쁜 것도 아니다. 다음은 정말 끔찍한 문서이다. 문제 중에서도, <FORM> 태그가 <TABLE> 태그 밖에서 시작해서 <TABLE> 태그 안에서 끝난다는 것이다. (이 HTML은 큰 웹 회사에서 운영하는 웹 사이트에서 발견하였다.)

from BeautifulSoup import BeautifulSoup
html = """
<html>
<form>
 <table>
 <td><input name="input1">Row 1 cell 1
 <tr><td>Row 2 cell 1
 </form> 
 <td>Row 2 cell 2<br>This</br> sure is a long cell
</body> 
</html>"""

뷰티플수프는 이 문서도 역시 잘 처리한다:

print BeautifulSoup(html).prettify()
# <html>
#  <form>
#   <table>
#    <td>
#     <input name="input1" />
#     Row 1 cell 1
#    </td>
#    <tr>
#     <td>
#      Row 2 cell 1
#     </td>
#    </tr>
#   </table>
#  </form>
#  <td>
#   Row 2 cell 2
#   <br />
#   This 
#   sure is a long cell
#  </td>
# </html>

테이블의 마지막 셀은 <TABLE> 태그 밖에 있다; 뷰티플수프는 <FORM> 태그를 닫을 때 <TABLE> 태그를 닫기로 결정했다. 원래 문서의 저자는 아마도 의도가 <FORM> 태그를 테이블의 끝까지 확장하는 것이었겠지만, 뷰티플수프는 그것을 알 방법이 없다. 이와 같이 괴상한 경우에도, 뷰티플수프는 무효한 문서를 해석해서 모든 데이터에 접근이 가능하게 해 준다.

XML 해석하기

BeautifulSoup 클래스는 HTML 저자의 의도를 존중해주는 웹-브라우져 비슷한 휴리스틱으로 가득하다. 그러나 XML은 태그 세트가 고정되어 있지 않다. 그래서 그런 휴리스틱은 적용되지 않는다. BeautifulSoup는 XML을 별로 잘 다루지 못한다.

BeautifulStoneSoup 클래스를 사용하여 XML 문서를 해석해 보자. 이 클래스는 특별히 XML 방언을 잘 알지 못하는 범용 클래스로서 태그 내포에 관하여 아주 간단한 규칙만 알 뿐이다. 다음은 작동하는 예이다:

from BeautifulSoup import BeautifulStoneSoup
xml = "<doc><tag1>Contents 1<tag2>Contents 2<tag1>Contents 3"
soup = BeautifulStoneSoup(xml)
print soup.prettify()
# <doc>
#  <tag1>
#   Contents 1
#   <tag2>
#    Contents 2
#   </tag2>
#  </tag1>
#  <tag1>
#   Contents 3
#  </tag1>
# </doc>

BeautifulStoneSoup가장 일반적인 단점은 단독-닫기 태그에 관하여 모른다는 것이다. HTML은 단독-닫기 태그가 고정되어 있지만, XML에서는 DTD에 의존한다. 어떤 태그들을 단독-닫기 태그라고 BeautifulStoneSoup에게 알려줄 수도 있다. 그 이름들을 selfClosingTags 인자로 구성자에게 건네면 된다:

from BeautifulSoup import BeautifulStoneSoup
xml = "<tag>Text 1<selfclosing>Text 2"
print BeautifulStoneSoup(xml).prettify()
# <tag>
#  Text 1
#  <selfclosing>
#   Text 2
#  </selfclosing>
# </tag>

print BeautifulStoneSoup(xml, selfClosingTags=['selfclosing']).prettify()
# <tag>
#  Text 1
#  <selfclosing />
#  Text 2
# </tag>

작동하지 않는다면

이 두 휴리스틱과 다른 휴리스틱이 있는 다른 해석 클래스도 여럿 있다. 또 해석기를 하부클래스화하여 재단해서 자신만의 휴리스틱을 제공할 수도 있다.

이런, 유니코드잖아

해석이 끝날 때 쯤이면 유니코드로 변환된다. 뷰티플수프는 Unicode 문자열로 데이터 구조를 저장한다.

from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup("Hello")
soup.contents[0]
# u'Hello'
soup.originalEncoding
# 'ascii'

다음은 일본어 문서를 UTF-8로 인코드한 예이다:

from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup("\xe3\x81\x93\xe3\x82\x8c\xe3\x81\xaf")
soup.contents[0]
# u'\u3053\u308c\u306f'
soup.originalEncoding
# 'utf-8'

str(soup)
# '\xe3\x81\x93\xe3\x82\x8c\xe3\x81\xaf'

# 주의: 이 부분은 EUC-JP를 사용하므로, cjkcodecs이 있어야 작동한다
# 또는 파이썬 2.4에서 실행해야 한다.
soup.__str__('euc-jp')
# '\xa4\xb3\xa4\xec\xa4\xcf'

뷰티플수프는 UnicodeDammit이라는 클래스를 사용하여 문서의 인코딩을 탐지해서 유니코드로 변환한다. 다른 문서에 대하여 이렇게 할 필요가 있다면 (뷰티플수프 없이 해석하려면), UnicodeDammit만 따로 사용해도 좋다. 범용 감 해석기(Universal Feed Parser) 코드에 크게 의존하고 있다.

파이썬 2.4 이전의 버전이라면, 꼭 cjkcodecsiconvcodec을 내려받아 설치하자. 이 꾸러미들이 있으면 코덱을 더 많이 지원하고, 특히 CJK 코덱을 지원한다. 또 자동으로 탐지하려면 chardet라이브러리도 설치하자.

뷰티플수프는 문서를 유니코드로 바꾸기 위하여 순서대로 다음 인코딩을 시도한다:

뷰티플수프는 언제나 올바로 추측할 것이다. 그러나 문서에 아무 선언이 없고 인코딩이 이상하면, 가끔 올바로 추측하지 못할 수도 있다. 그 때는 Windows-1252에 의존하는데, 아마도 이것은 올바르지 못할 것이다. 다음은 EUC-JP 예제인데 뷰티플수프는 인코딩을 잘못 추측한다. (역시, EUC-JP를 사용하기 때문에, 이 예제는 파이썬 2.4이상이나 cjkcodecs 코덱이 설치되어 있어야 작동한다):

from BeautifulSoup import BeautifulSoup
euc_jp = '\xa4\xb3\xa4\xec\xa4\xcf'

soup = BeautifulSoup(euc_jp)
soup.originalEncoding
# 'windows-1252'

str(soup)
# '\xc2\xa4\xc2\xb3\xc2\xa4\xc3\xac\xc2\xa4\xc3\x8f'     # 잘못!

그러나 fromEncoding으로 인코딩을 지정하면, 문서를 올바로 해석해서, UTF-8이나 다시 EUC-JP로 변환할 수 있다.

soup = BeautifulSoup(euc_jp, fromEncoding="euc-jp")
soup.originalEncoding
# 'windows-1252'

str(soup)
# '\xe3\x81\x93\xe3\x82\x8c\xe3\x81\xaf'                 # 맞음!

soup.__str__(self, 'euc-jp') == euc_jp
# True

뷰티플수프에 인코딩이 Windows-1252인 문서를 먹이면 (또는 비슷한 인코딩으로 ISO-8859-1 또는 ISO-8859-2), 뷰티플수프는 그 문서의 지능형 따옴표와 기타 윈도우즈-전용의 문자들을 찾아 폐기한다. 그런 문자들을 동등한 유니코드로 변환하기 보다, 뷰티플수프는 그런 문자들을 HTML 개체(BeautifulSoup) 또는 XML 개체(BeautifulStoneSoup) 변환한다.

이를 막으려면, smartQuotesTo=None를 수프 구성자에게 건네면 된다: 그러면 지능형 따옴표는 다른 고유의-인코딩 문자들과 마찬가지로 유니코드로 변환된다. 또 "xml"이나 "html"을 smartQuotesTo에 건네어, BeautifulSoupBeautifulStoneSoup의 기본 행위를 바꿀 수 있다.

from BeautifulSoup import BeautifulSoup, BeautifulStoneSoup
text = "Deploy the \x91SMART QUOTES\x92!"

str(BeautifulSoup(text))
# 'Deploy the &lsquo;SMART QUOTES&rsquo;!'

str(BeautifulStoneSoup(text))
# 'Deploy the &#x2018;SMART QUOTES&#x2019;!'

str(BeautifulSoup(text, smartQuotesTo="xml"))
# 'Deploy the &#x2018;SMART QUOTES&#x2019;!'

BeautifulSoup(text, smartQuotesTo=None).contents[0]
# u'Deploy the \u2018SMART QUOTES\u2019!'

문서 인쇄하기

뷰티플수프 문서 (또는 그의 일부)를 문자열로 바꿀 수 있다. str 함수나 prettify 또는 renderContents 메쏘드로 말이다. 또 unicode 함수를 사용하면 전체 문서를 유니코드 문자열로 변환할 수 있다.

prettify 메쏘드는 전략적으로 새줄문자와 공백문자를 추가하여 문서의 구조를 확실하게 들어낸다. 또 공백만 들어 있는 텍스트 노드를 걷어내는데, 이 때문에 XML 문서의 의미가 바뀔 수도 있다. str 함수와 unicode 함수는 공백만 들어있더라도 그 텍스트 노드를 걷어내지 않으며, 노드 사이에 공백을 추가하지도 않는다.

다음은 예제이다.

from BeautifulSoup import BeautifulSoup
doc = "<html><h1>Heading</h1><p>Text"
soup = BeautifulSoup(doc)

str(soup)
# '<html><h1>Heading</h1><p>Text</p></html>'
soup.renderContents()
# '<html><h1>Heading</h1><p>Text</p></html>'
soup.__str__()
# '<html><h1>Heading</h1><p>Text</p></html>'
unicode(soup)
# u'<html><h1>Heading</h1><p>Text</p></html>'

soup.prettify()
# '<html>\n <h1>\n  Heading\n </h1>\n <p>\n  Text\n </p>\n</html>'

print soup.prettify()
# <html>
#  <h1>
#   Heading
#  </h1>
#  <p>
#   Text
#  </p>
# </html>

문서 안의 태그에 사용되면 str 함수와 renderContents는 결과가 다르다는데 주의하자. str 함수는 태그와 그의 내용을 인쇄하고, renderContents 함수는 내용만을 인쇄한다.

heading = soup.h1
str(heading)
# '<h1>Heading</h1>'
heading.renderContents()
# 'Heading'

__str__이나 prettify 또는 renderContents를 호출할 때, 출력 인코딩을 지정할 수 있다. (str이 사용하는) 기본 인코딩은 UTF-8이다. 다음은 ISO-8851-1 문자열을 해석한 다음 같은 문자열을 다른 인코딩을 출력하는 예이다:

from BeautifulSoup import BeautifulSoup
doc = "Sacr\xe9 bleu!"
soup = BeautifulSoup(doc)
str(soup)
# 'Sacr\xc3\xa9 bleu!'                          # UTF-8
soup.__str__("ISO-8859-1")
# 'Sacr\xe9 bleu!'
soup.__str__("UTF-16")
# '\xff\xfeS\x00a\x00c\x00r\x00\xe9\x00 \x00b\x00l\x00e\x00u\x00!\x00'
soup.__str__("EUC-JP")
# 'Sacr\x8f\xab\xb1 bleu!'

원래 문서에 인코딩 선언이 있으면, 뷰티플수프는 문서를 문자열로 변환할 때 그 선언을 다시 사용하여 새로운 인코딩을 명시한다 . 즉 HTML 문서를 BeautifulSoup로 적재하여 다시 인쇄하면, 그 HTML은 깨끗이 청소가 될 뿐만 아니라, 투명하게 UTF-8로 변환된다는 뜻이다.

다음은 HTML 예제이다:

from BeautifulSoup import BeautifulSoup
doc = """<html>
<meta http-equiv="Content-type" content="text/html; charset=ISO-Latin-1" >
Sacr\xe9 bleu!
</html>"""

print BeautifulSoup(doc).prettify()
# <html>
#  <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
#  Sacre bleu!
# </html>

다음은 XML 예제이다:

from BeautifulSoup import BeautifulStoneSoup
doc = """<?xml version="1.0" encoding="ISO-Latin-1">Sacr\xe9 bleu!"""

print BeautifulStoneSoup(doc).prettify()
# <?xml version='1.0' encoding='utf-8'>
# Sacre bleu!

해석 트리(Parse Tree)

지금까지 문서를 적재하고 그것을 다시 출력하는데 중점을 두어왔다. 그렇지만, 여러분의 관심은 해석 트리에 있을 것이다: 문서를 해석하면서 뷰티플수프가 구축하는 데이터 구조에 말이다.

(BeautifulSoupBeautifulStoneSoup의 실체인) 해석기 객체는 내포가 깊고 연결이 잘된 데이터 구조로서 XML이나 HTML 문서의 구조에 상응한다. 해석기 객체에는 두 가지 유형의 객체가 있다: Tag 객체는 <TITLE> 태그와 <B> 태그 같은 구조에 상응한다; NavigableString 객체는 "Page title" 그리고 "This is paragraph" 같은 문자열에 상응한다.

또한 NavigableString의 하부클래스도 있는데 (CData, Comment, Declaration, 그리고 ProcessingInstruction), 이 하부클래스들은 특수한 XML 구성물에 상응한다. 인쇄할 때, 여분의 데이터가 더 붙는다는 점만 제외하면 NavigableString처럼 작동한다. 다음은 주석이 달린 문서이다:

from BeautifulSoup import BeautifulSoup
import re
hello = "Hello! <!--I've got to be nice to get what I want.-->"
commentSoup = BeautifulSoup(hello)
comment = commentSoup.find(text=re.compile("nice"))

comment.__class__
# <class 'BeautifulSoup.Comment'>
comment
# u"I've got to be nice to get what I want."
comment.previousSibling
# u'Hello! '

str(comment)
# "<!--I've got to be nice to get what I want.-->"
print commentSoup
# Hello! <!--I've got to be nice to get what I want.-->

이제, 앞에서 사용된 문서를 더 자세히 살펴보자:

from BeautifulSoup import BeautifulSoup 
doc = ['<html><head><title>Page title</title></head>',
       '<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',
       '<p id="secondpara" align="blah">This is paragraph <b>two</b>.',
       '</html>']
soup = BeautifulSoup(''.join(doc))

print soup.prettify()
# <html>
#  <head>
#   <title>
#    Page title
#   </title>
#  </head>
#  <body>
#   <p id="firstpara" align="center">
#    This is paragraph
#    <b>
#     one
#    </b>
#    .
#   </p>
#   <p id="secondpara" align="blah">
#    This is paragraph
#    <b>
#     two
#    </b>
#    .
#   </p>
#  </body>
# </html>

Tag 속성

Tag 객체와 NavigableString 객체에는 수 많은 구성원이 있는데, 대부분 해석 트리 항해하기해석 트리 검색하기에서 다룬다. 그렇지만, Tag 객체의 한 측면은 여기에서 다루겠다: 속성들을 다루어 보자.

SGML 태그는 속성이 있다: 예를 들면, 위의 HTML 예제에서 각각의 <P> 태그는 "id" 속성과 "align" 속성이 있다. 태그의 속성에 접근하려면 Tag 객체를 그냥 사전처럼 취급하면 된다:

firstPTag, secondPTag = soup.find_all('p')

firstPTag['id']
# u'firstPara'

secondPTag['id']
# u'secondPara'

NavigableString 객체는 속성이 없다; Tag 객체만 그 속성이 있다.

해석 트리 항해하기

모든 Tag 객체는 아래에 나열된 멤버를 모두 가진다 (그렇지만 구성원의 실제 값은 None일 수 있다). NavigableString 객체는 contentsstring을 빼고 모든 것이 있다.

parent

위의 예제에서, <HEAD> Tag의 부모는 <HTML> Tag이다. <HTML>Tag의 부모는 BeautifulSoup 해석기 객체이다. 해석기 객체의 부모는 None이다. parent를 따라가면, 해석 트리를 기어오를 수 있다:

soup.head.parent.name
# u'html'
soup.head.parent.parent.__class__.__name__
# 'BeautifulSoup'
soup.parent == None
# True

contents

parent로 해석 트리를 기어 오른다. contents로 해석 트리를 내려 온다. contents는 페이지 요소 안에 포함되어 있는 Tag 객체와 NavigableString 객체의 순서있는 리스트이다. 오직 최고-수준의 해석기 객체와 Tag 객체만이 contents가 있다. NavigableString 객체는 그저 문자열이며 하부-요소를 가질 수 없다. 그래서 contents가 없다.

위의 예제에서, 첫 <P> TagcontentsNavigableString("This is paragraph ")과, <B> Tag 한개 그리고 또다른 NavigableString (".")이 담긴 리스트이다. <B> Tagcontents는 : NavigableString ("one")이 담긴 리스트이다.

pTag = soup.('p')
pTag.contents
# [u'This is paragraph ', <b>one</b>, u'.']
pTag.contents[1].contents
# [u'one']
pTag.contents[0].contents
# AttributeError: 'NavigableString' object has no attribute 'contents'

string

편리함을 위하여, 태그에 오직 자손 노드가 하나이고, 그 자손 노드가 문자열이라면, 그 자손 노드를 tag.string이나 tag.contents[0]으로 접근할 수 있다. 위의 예제에서, soup.b.stringNavigableString으로서 유니코드 문자열 "one"을 나타낸다. 해석 트리에서 첫 <B> Tag에 담긴 문자열이다.

soup.b.string
# u'one'
soup.b.contents[0]
# u'one'

그러나 soup.p.stringNone인데, 해석 트리에서 첫 <P> Tag에 자손이 하나가 아니기 때문이다. soup.head.string도 역시 None이다. <HEAD> 태그에 자손이 하나 밖에 없지만, 그 자손이 NavigableString이 아니라 Tag (<TITLE> Tag)이기 때문이다.

soup.p.string == None
# True
soup.head.string == None
# True

nextSibling 그리고 previousSibling

다음 멤버들을 사용하면 해석 트리에서 같은 수준을 앞 뒤로 이동할 수 있다. 위의 예제에서, <HEAD> TagnextSibling은 <BODY> Tag인데, <BODY> Tag는 <html> Tag 바로 아래에 있는 다음 노드이기 때문이다. <BODY> 태그의 nextSiblingNone인데, <HTML>Tag 바로 아래에 아무것도 없기 때문이다.

soup.head.nextSibling.name
# u'body'
soup.html.nextSibling == None
# True

반대로, <BODY> TagpreviousSibling은 <HEAD> 태그이다. <HEAD> TagpreviousSiblingNone이다:

soup.body.previousSibling.name
# u'head'
soup.head.previousSibling == None
# True

예제를 조금 더 살펴보자: <P> TagnextSibling은 두 번째 <P> Tag이다. <P> Tag 안에 있는 <B> TagpreviousSiblingNavigableString "This is paragraph"이다. 그 NavigableStringpreviousSiblingNone이다. 앞 <P> Tag 안에 아무것도 없기 때문이다.

soup.p.nextSibling
# <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>

secondBTag = soup.find_all('b')[1]
secondBTag.previousSibling
# u'This is paragraph'
secondBTag.previousSibling.previousSibling == None
# True

next 그리고 previous

다음 멤버를 사용하면 트리에 보이는 순서가 아닌, 해석기가 처리한 순서대로 문서 요소를 방문할 수 있다. 예를 들면, <HEAD> Tagnext는 <BODY> Tag가 아니라, <TITLE> Tag이다. 이는 원래 문서에서, <TITLE> 태그가 <HEAD> 태그 바로 다음에 오기 때문이다.

soup.head.next
# u'title'
soup.head.nextSibling.name
# u'body'
soup.head.previous.name
# u'html'

nextprevious를 사용하면, Tagcontents이 그의 nextSibling보다 먼저 온다. 보통은 이런 멤버들을 사용할 필요가 없겠지만, 가끔은 해석 트리 깊숙히 묻힌 것을 얻는 제일 쉬운 방법이다.

Tag

순회하기

Tagcontents를 리스트로 취급하여 반복할 수 있다. 이는 유용한 지름길이다. 비슷하게, 얼마나 많은 자손노드가 Tag에 있는지 알고 싶으면, len(tag.contents) 대신에 len(tag)를 호출하면 된다. 위의 예제 참조:

for i in soup.body:
    print i
# <p id="firstpara" align="center">This is paragraph <b>one</b>.</p>
# <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>

len(soup.body)
# 2
len(soup.body.contents)
# 2

태그 이름을 멤버로 사용하기

원하는 태그의 이름이 마치 해석기 멤버나 Tag 객체처럼 행위하면 쉽게 해석 트리를 항해할 수 있다. 이런 예들을 통하여 그렇게 해 오고 있다. 위의 예제에서, soup.head는 문서에서 (당연하게도, 유일한) 첫 <HEAD> Tag를 돌려준다:

soup.head
# <head><title>Page title</title></head>

일반적으로, mytag.foo를 호출하면 mytag의 첫 자손이 반환되는데, 여기에서는 <FOO> Tag이다. <FOO> Tag 태그가 mytag에 없다면, mytag.fooNone을 돌려준다. 다음과 같이 해석 트리를 아주 빠르게 순회할 수 있다:

soup.head.title
# <title>Page title</title>

soup.body.p.b.string
# u'one'

또 다음과 같이 아주 빠르게 해석 트리의 특정한 부분으로 점프해 들어갈 수 있다. 예를 들어, <TITLE> 태그가 괴이하게 <HEAD> 태그 밖에 있더라도 신경쓰지 않는다면, 그냥 soup.title를 사용하여 HTML 문서의 제목을 얻을 수 있다. soup.head.title를 사용할 필요가 없다:

soup.title.string
# u'Page title'

soup.p는 첫 <P> 태그로 점프한다. 어디에 있는지에 상관없이 말이다. soup.table.tr.td는 첫 테이블 첫 행의 첫 컬럼으로 점프한다.

이 멤버들은 실제로는 아래에 기술한 first 메쏘드의 별칭이다. 여기에서 언급하는 이유는 그 별칭 덕분에 잘-알려진 해석트리에서 관심 부분을 아주 쉽게 확대하여 볼 수 있기 때문이다.

이 관용구는 다른 형태도 있다. .foo 대신에 .fooTag의 형태로 그 첫 <FOO> 태그에 접근할 수 있다. 예를 들어, soup.table.tr.tdsoup.tableTag.trTag.tdTag로 표현되거나, 심지어 soup.tableTag.tr.tdTag로 표현해도 된다. 이는 무엇을 하고 있는지 좀 더 명시적으로 지정하고 싶은 경우 유용하다. 또는 태그 이름이 뷰티플수프의 메쓰드 그리고 멤버와 충돌하는 XML을 해석하는 경우에도 유용하다.

from BeautifulSoup import BeautifulStoneSoup
xml = '<person name="Bob"><parent rel="mother" name="Alice">'
xmlSoup = BeautifulStoneSoup(xml)

xmlSoup.person.parent                      # 뷰티플수프 멤버
# <person name="Bob"><parent rel="mother" name="Alice"></parent></person>
xmlSoup.person.parentTag                   # 태그 이름
# <parent rel="mother" name="Alice"></parent>

해석 트리 탐색하기

뷰티플수프는 해석 트리를 순회하는 많은 방법을 제공한다. 지정된 기준에 부합하는 TagNavigableString을 수집한다.

뷰티플수프 객체에 부합하는 기준을 정의하는 방법은 여러가지가 있다. 자세하게 가장 기본적인 뷰티플수프 탐색 메쏘드인 findAll을 살펴보겠다. 예와 같이, 다음 문서에서 보여주겠다:

from BeautifulSoup import BeautifulSoup
doc = ['<html><head><title>Page title</title></head>',
       '<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',
       '<p id="secondpara" align="blah">This is paragraph <b>two</b>.',
       '</html>']
soup = BeautifulSoup(''.join(doc))
print soup.prettify()
# <html>
#  <head>
#   <title>
#    Page title
#   </title>
#  </head>
#  <body>
#   <p id="firstpara" align="center">
#    This is paragraph
#    <b>
#     one
#    </b>
#    .
#   </p>
#   <p id="secondpara" align="blah">
#    This is paragraph
#    <b>
#     two
#    </b>
#    .
#   </p>
#  </body>
# </html>

우연하게도, 이 섹션에서 기술한 두 메쏘드 (findAll 그리고 find)는 NavigableString 객체가 아니라 Tag 객체와 최고-수준의 해석기 객체에만 사용이 가능하다. 해석 트리 안에서 검색하기에 정의된 메쏘드들은 NavigableString 객체에 사용이 가능하다.

기본 검색 메쏘드: findAll(name, attrs, recursive, text, limit, **kwargs)

findAll 메쏘드는 트리를 순회하는데, 주어진 노드에서 시작하여, 지정한 기준에 부합하는 TagNavigableString 객체를 모두 찾는다. findall 메쏘드의 서명은 다음과 같다:

findAll(name=None, attrs={}, recursive=True, text=None, limit=None, **kwargs)

다음 인자들은 뷰티플수프 API에서 나타나고 또 나타난다. 가장 기본적인 인자는 name과 키워드 인자들이다.

태그를 호출하는 것은 findall을 호출하는 것과 같다

여러분의 수고를 덜어준다. 해석기 객체 또는 함수 같은 Tag를 호출하면, findall의 모든 인자들을 건넬 수 있고 그것은 findall을 호출한 것과 똑 같다. 위의 예제 참조:

soup(text=lambda(x): len(x) < 12)
# [u'Page title', u'one', u'.', u'two', u'.']

soup.body('p', limit=1)
# [<p id="firstpara" align="center">This is paragraph <b>one</b>.</p>]

find(name, attrs, recursive, text, **kwargs)

좋다. 이제 다른 탐색 메쏘드를 살펴보자. 다른 메쏘드들도 모두 findAll과 거의 같은 인자를 취한다.

find 메쏘드는 거의 정확하게 findAll과 같다. 단, 부합된 객체를 모두 찾는 대신에, 오직 부합된 첫 객체만 찾는다는 점만 빼고 말이다. 마치 검색 결과 집합에 제한을 1로 두고, 그 배열에서 단 하나의 결과만 추출하는 것과 같다. 위의 문서 참조:

soup.findAll('p', limit=1)
# [<p id="firstpara" align="center">This is paragraph <b>one</b>.</p>]

soup.find('p', limit=1)
# <p id="firstpara" align="center">This is paragraph <b>one</b>.</p>

soup.find('nosuchtag', limit=1) == None
# True

일반적으로, 복수형으로 된 이름을 가진 검색 메쏘드는 (이를테면 findAll이나 findNextSiblings 같은) limit 인자를 취하고 검색 결과를 돌려준다. 이름이 복수형이 아닌 검색 메쏘드는 (예를 들어 findfindNextSibling 같은) limit 인자를 취하지 않으며 결과는 단 한개를 돌려준다.

first에 무슨 일이?

이전의 뷰티플수프 버전에는 first, fetch, 그리고 fetchPrevious 같은 메쏘드들이 있었다. 이 메쏘드들은 여전히 있지만, 추천하지 않는다. 그리고 조만간 사라질 것이다. 전반적으로 이름이 주는 효과가 아주 혼란스러웠다. 새로운 이름은 일관성있게 지었다: 위에 언급한 바와 같이, 메쏘드 이름이 복수형이거나 All을 가리키면, 여러 객체를 돌려준다. 그렇지 않으면, 객체 하나를 돌려준다.

해석 트리 안에서 탐색하기

위에 기술한 메쏘드인 findAllfind는 해석 트리의 특정 지점에서 시작해 아래오 내려온다. 바닥날 때까지 객체의 내용물(contents)을 재귀적으로 순회한다.

이는 곧 이런 메쏘드드들에 NavigableString 객체에 요청할 수 없다는 뜻이다. 왜냐하면 내용물(contents)이 없기 때문이다: 언제나 해석트리에서 말단 노드들이다.

그러나 문서를 순회하기 위하여 내려가는 것만이 유일한 방법은 아니다. 앞서 해석 트리 항해하기에서 다른 방법들을 많이 보여주었다: parent, nextSibling, 등등. 이런 반복 테크닉들은 각각 상응하는 두 가지 메쏘드가 있다: 하나는 findAll처럼 작동하며, 또 하나는 find처럼 작동한다. NavigableString 객체들이 이런 연산들을 지원하므로, 이런 메쏘드들을NavigableString 객체들 뿐만 아니라 Tag 객체와 주 해석 객체에 요청해도 된다.

왜 이것이 유용한가? 음, 어떤 경우는 그냥 findAll이나 find를 써서는 원하는 TagNavigableString을 얻을 수 없다. 예를 들어, 다음과 같은 HTML을 생각해 보자:

from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup('''<ul>
 <li>An unrelated list
</ul>

<h1>Heading</h1>
<p>This is <b>the list you want</b>:</p>
<ul><li>The data you want</ul>''')

원하는 데이터가 든 <LI> 태그를 항해하는 방식은 수 없이 많다. 가장 확실한 방법은 다음과 같다:

soup('li', limit=2)[1]
# <li>The data you want</li>

그러나 이것 역시 그 <LI> 태그를 얻는데 별로 안정적인 방법은 아니다. 이 페이지를 단 한번만 스크랩할 생각이라면 문제가 되지 않지만, 여러번 오랜 기간에 걸쳐 스크랩할 생각이라면, 주의해야 한다. 그 부적절한 리스트에 또 <LI> 태그가 생기면, 원하는 태그가 아니라 바로 그 태그를 얻게 될 것이다. 그러므로 스크립트가 망가지거나 엉뚱한 데이터를 돌려줄 것이다.

soup('ul', limit=2)[1].li
# <li>The data you want</li>

좀 더 개선되었다. 부적절한 리스트의 변화에 견딜 수 있기 때문이다. 그러나 문서의 상단에 부적절한 리스트가 또 생기면, 워하는 것 대신에 그 리스트의 첫 <LI> 태그를 얻게 될 것이다. 원하는 ul 태그를 참조하는 좀 더 믿을만 한 방법은 그 태그의 위치를 문서의 구조에 반영하는 것이다.

이 HTML을 보면, 원하는 리스트를 <H1> 태그 아래에 있는 <UL> 태그라고 간주할 것이다. 문제는 그 태그가 <H1> 태그 사이에 있지 않다는 것이다; 그저 어쩌다가 그 뒤에 따라올 뿐이다. 쉽게 <H1> 태그를 얻을 수 있지만, 거기에서부터 <UL> 태그에 도달할 방법은 없다. firstfetch를 사용하면 될 것 같지만 안된다. 왜냐하면 그런 메쏘드들은 <H1> 태그의 내용만을 검색하기 때문이다. nextnextSibling 멤버로 <UL> 태그를 항해할 필요가 있다:

s = soup.h1
while getattr(s, 'name', None) != 'ul':
    s = s.nextSibling
s.li
# <li>The data you want</li>

그렇지 않고, 다음이 더 안정적이라고 생각된다면:

s = soup.find(text='Heading')
while getattr(s, 'name', None) != 'ul':
    s = s.next
s.li
# <li>The data you want</li>

그러나 당연히 탐험에 들어야할 수고보다 더 고생스럽다. 이 섹션에서 다룬 메쏘드들은 일손을 덜어준다. 항해 멤버중의 하나에 회돌이를 작성하고 싶을 때마다 사용이 가능하다. 해석 트리의 일정 위치에 시작 지점을 주면, 트리를 항해하면서 지정한 기준에 부합하는 TagNavigableString 객체들을 수집한다. 위의 예제 코드의 첫 회돌이 대신에, 그냥 다음과 같이 작성하면 된다:

soup.h1.findNextSibling('ul').li
# <li>The data you want</li>

두 번째 회돌이 대신, 다음과 같이 작성해도 된다:

soup.find(text='Heading').findNext('ul').li
# <li>The data you want</li>

회돌이는 findNextSiblingfindNext에 대한 호출로 교체되었다. 다음 섹션은 이런 종류의 메쏘드들을 모두 다룬다. 역시, 모든 항해 멤버 각각에 대하여 두 가지 메쏘드가 있다: 하나는 findAll의 방식으로 리스트를 돌려주고, 또 하나는 find의 방식으로 스칼라를 돌려준다.

마지막으로, 예를 들어 보기 위하여 친숙한 수프 문서를 적재해 보자:

from BeautifulSoup import BeautifulSoup
doc = ['<html><head><title>Page title</title></head>',
       '<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',
       '<p id="secondpara" align="blah">This is paragraph <b>two</b>.',
       '</html>']
soup = BeautifulSoup(''.join(doc))
print soup.prettify()
# <html>
#  <head>
#   <title>
#    Page title
#   </title>
#  </head>
#  <body>
#   <p id="firstpara" align="center">
#    This is paragraph
#    <b>
#     one
#    </b>
#    .
#   </p>
#   <p id="secondpara" align="blah">
#    This is paragraph
#    <b>
#     two
#    </b>
#    .
#   </p>
#  </body>
# </html>

findNextSiblings(name, attrs, text, limit, **kwargs) and findNextSibling(name, attrs, text, **kwargs)

다음 메쏘드들은 반복적으로 한 객체의 nextSibling 멤버를 따라가면서, 지정한 기준에 부합하는 TagNavigableText 객체들을 수집한다. 위의 예제 참조:

paraText = soup.find(text='This is paragraph ')
paraText.findNextSiblings('b')
# [<b>one</b>]

paraText.findNextSibling(text = lambda(text): len(text) == 1)
# u'.'

findPreviousSiblings(name, attrs, text, limit, **kwargs) and findPreviousSibling(name, attrs, text, **kwargs)

다음 메쏘드들은 반복적으로 한 객체의 previousSibling 멤버를 따라가면서, 지정한 기준에 부합하는 TagNavigableText 객체들을 수집한다. 위의 예제 참조:

paraText = soup.find(text='.')
paraText.findPreviousSiblings('b')
# [<b>one</b>]

paraText.findPreviousSibling(text = True)
# u'This is paragraph '

findAllNext(name, attrs, text, limit, **kwargs) and findNext(name, attrs, text, **kwargs)

다음 메쏘드들은 반복적으로 한 객체의 next 멤버를 따라가면서, 지정한 기준에 부합하는 TagNavigableText 객체들을 수집한다. 위의 예제 참조:

pTag = soup.find('p')
pTag.findAllNext(text=True)
# [u'This is paragraph ', u'one', u'.', u'This is paragraph ', u'two', u'.']

pTag.findNext('p')
# <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>

pTag.findNext('b')
# <b>one</b>

findAllPrevious(name, attrs, text, limit, **kwargs) and findPrevious(name, attrs, text, **kwargs)

이런 메쏘드들은 반복적으로 한 객체의 previous 멤버를 따라가면서, 지정한 기준에 부합하는 TagNavigableText 객체들을 수집한다. 위의 예제 참조:

lastPTag = soup('p')[-1]
lastPTag.findAllPrevious(text=True)
# [u'.', u'one', u'This is paragraph ', u'Page title']
# Note the reverse order!

lastPTag.findPrevious('p')
# <p id="firstpara" align="center">This is paragraph <b>one</b>.</p>

lastPTag.findPrevious('b')
# <b>one</b>

findParents(name, attrs, limit, **kwargs) and findParent(name, attrs, **kwargs)

다음 메소드들은 반복적으로 한 객체의 parent 멤버를 따라가면서, 지정한 기분에 부합하는 TagNavigableText 객체들을 수집한다. text 인자는 취하지 않는데, 그 이유는 어떤 객체도 부모 노드에 대하여 NavigableString을 가질수 없기 때문이다. 위의 문서 참조:

bTag = soup.find('b')

[tag.name for tag in bTag.findParents()]
# [u'p', u'body', u'html', '[document]']
# NOTE: "u'[document]'" means that that the parser object itself matched.

bTag.findParent('body').name
# u'body'

해석 트리 수정하기

이제 해석 트리를 검색하는 법을 알았다. 그러나 수정하고 인쇄하는 법도 알고 싶을 것이다. 한 요소를 그의 부모의 내용(contents)에서 뽑아 낼 수는 있지만, 나머지 문서에서는 여전히 뽑아 낸 그 요소를 참조할 것이다. 뷰티플수프는 내부적 일관성을 유지하면서 해석 트리를 수정하는 방법을 여러가지 제공한다.

속성 값 바꾸기

사전 할당을 사용하면 Tag 객체의 속성 값을 수정할 수 있다.

from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup("<b id="2">Argh!</b>")
print soup
# <b id="2">Argh!</b>
b = soup.b

b['id'] = 10
print soup
# <b id="10">Argh!</b>

b['id'] = "ten"
print soup
# <b id="ten">Argh!</b>

b['id'] = 'one "million"'
print soup
# <b id='one "million"'>Argh!</b>

또 속성 값을 지우고, 새로운 값을 추가할 수도 있다:

del(b['id'])
print soup
# <b>Argh!</b>

b['class'] = "extra bold and brassy!"
print soup
# <b class="extra bold and brassy!">Argh!</b>

요소 제거하기

한 요소의 참조점을 알면, extract 메쏘드로 해석트리에서 그 요소를 뽑아낼 수 있다. 다음 코드는 문서로부터 모든 주석을 제거한다:

from BeautifulSoup import BeautifulSoup, Comment
soup = BeautifulSoup("""1<!--The loneliest number-->
                        <a>2<!--Can be as bad as one--><b>3""")
comments = soup.findAll(text=lambda text:isinstance(text, Comment))
[comment.extract() for comment in comments]
print soup
# 1
# <a>2<b>3</b></a>

다음 코드는 문서로부터 한 하부트리를 통채로 제거한다:

from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup("<a1></a1><a><b>Amazing content<c><d></a><a2></a2>")
soup.a1.nextSibling
# <a><b>Amazing content<c><d></d></c></b></a>
soup.a2.previousSibling
# <a><b>Amazing content<c><d></d></c></b></a>

subtree = soup.a
subtree.extract()

print soup
# <a1></a1><a2></a2>
soup.a1.nextSibling
# <a2></a2>
soup.a2.previousSibling
# <a1></a1>

extract 메소드는 한 해석 트리를 두 개의 트리로 분할한다. 항해 멤버들은 마치 전에는 전혀 함께 하지 않았던 것처럼 변한다:

soup.a1.nextSibling
# <a2></a2>
soup.a2.previousSibling
# <a1></a1>
subtree.previousSibling == None
# True
subtree.parent == None
# True

한 요소를 또다른 요소로 교체하기

replaceWith 메쏘드는 한 페이지 요소를 추출하고 그것을 다른 요소로 (그 아래에 있는 전체 해석 트리로) 교체한다. 새 요소는 TagNavigableString이 될 수 있다. 평범한 구형 문자열을 replaceWith에 건네면, 그 문자열은 NavigableString으로 변한다. 그 문서에 마치 처음부터 그렇게 해석되어 있었던 것처럼 말이다.

다음은 간단한 예이다:

from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup("<b>Argh!</b>")
soup.find(text="Argh!").replaceWith("Hooray!")
print soup
# <b>Hooray!</b>

newText = soup.find(text="Hooray!")
newText.previous
# <b>Hooray!</b>
newText.previous.next
# u'Hooray!'
newText.parent
# <b>Hooray!</b>
soup.b.contents
# [u'Hooray!']

다음은 한 태그를 다른 태그로 교체하는 보다 복잡한 예이다:

from BeautifulSoup import BeautifulSoup, Tag
soup = BeautifulSoup("<b>Argh!<a>Foo</a></b><i>Blah!</i>")
tag = Tag(soup, "newTag", [("id", 1)])
tag.insert(0, "Hooray!")
soup.a.replaceWith(tag)
print soup
# <b>Argh!<newTag id="1">Hooray!</newTag></b><i>Blah!</i>

문서의 일부로부터 한 요소를 추출하여 그것을 또다른 부분에 붙일 수도 있다:

from BeautifulSoup import BeautifulSoup
text = "<html>There's <b>no</b> business like <b>show</b> business</html>"
soup = BeautifulSoup(text)

no, show = soup.findAll('b')
show.replaceWith(no)
print soup
# <html>There's  business like <b>no</b> business</html>

새로운 요소 추가하기

Tag 클래스와 해석기 클래스는 insert라고 부르는 메쏘드를 지원한다. 이 메쏘드는 파이썬 리스트의 insert 메쏘드처럼 작동한다: 태그의 contents 멤버에 대하여 인덱스를 취해, 새로운 요소를 슬롯에 끼워 넣는다.

이것은 앞 섹션에서 보여주었다. 문서에서 한 태그를 새로운 태그로 교체하였다. insert를 사용하면 전체 해석 트리를 처음부터 새로 만들 수 있다:

from BeautifulSoup import BeautifulSoup, Tag, NavigableString
soup = BeautifulSoup()
tag1 = Tag(soup, "mytag")
tag2 = Tag(soup, "myOtherTag")
tag3 = Tag(soup, "myThirdTag")
soup.insert(0, tag1)
tag1.insert(0, tag2)
tag1.insert(1, tag3)
print soup
# <mytag><myOtherTag></myOtherTag><myThirdTag></myThirdTag></mytag>

text = NavigableString("Hello!")
tag3.insert(0, text)
print soup
# <mytag><myOtherTag></myOtherTag><myThirdTag>Hello!</myThirdTag></mytag>

한 요소는 해석 트리에서 단 한 곳에만 나타날 수 있다. 수프 객체에 이미 접속되어 있는 요소를 insert에 넘기면, 먼저 (extract로) 분리된 후에, 다른 곳에서 연결된다. 이 예제에서는 NavigableString을 수프의 두 번째 부분에 삽입하고자 했지만, 또 삽입되지는 않는다. 이동되었다:

tag2.insert(0, text)
print soup
# <mytag><myOtherTag>Hello!</myOtherTag><myThirdTag></myThirdTag></mytag>

이런 일은 그 요소가 이미 완전히 다른 수프 객체에 속해 있을 경우에도 일어난다. 한 요소는 오직 하나의 parent, 하나의 nextSibling, 등등만 가진다. 그래서 한 번에 한 곳에만 있을 수 있다.

문제해결

다음 섹션은 뷰티플수프에서 사람들이 흔히 마주하는 문제들을 다룬다.

왜 뷰티플수프가 비-ASCII 문자들을 토해낼까요t?

다음과 같은 에러를 마주하신다면: "'ascii' codec can't encode character 'x' in position y: ordinal not in range(128)", 문제는 뷰티플수프가 아니라 파이썬 설치에 있을 것이다. 비-ASCII 문자들을 뷰티플수프 없이 인쇄해보자. 그러면 똑 같은 문제에 봉착할 것이다. 예를 들어, 다음과 같은 코드를 실행해보자:

latin1word = 'Sacr\xe9 bleu!'
unicodeword = unicode(latin1word, 'latin-1')
print unicodeword

이것은 작동하는데 뷰티플수프는 그렇지 않다면, 아마도 뷰티플수프에 버그가 있을 것이다. 그렇지만, 이것이 작동하지 않는다면, 그 문제는 파이썬 설정방법에 있다. 파이썬은 안전을 위하여 비 non-ASCII 문자들을 터미날에 전송하지 않는다. 두 가지 방법으로 이 행위를 오버라이드할 수 있다.

  1. 쉬운 방법은 ISO-Latin-1이나 UTF-8 문자들을 터미날에 보내는데 주저하지 않는 변환기에 표준 출력을 짝지워 주는 것이다.

    import codecs
    import sys
    streamWriter = codecs.lookup('utf-8')[-1]
    sys.stdout = streamWriter(sys.stdout)
    

    codecs.lookup은 코덱에 관련된 수 많은 바운드 메쏘드와 기타 객체들을 돌려준다. 앞의 것은 StreamWriter 객체로서 출력 스트림을 포장할 수 있다.

  2. 어려운 방법은 sitecustomize.py 파일을 파이썬 설치본에 만들어서 기본 인코딩을 ISO-Latin-1이나 UTF-8로 설정하는 것이다. 그러면 모든 파이썬 프로그램이 그 인코딩을 표준 출력에 사용할 것이다. 프로그램마다 손을 댈 필요가 없다. 나는 다음과 같이 /usr/lib/python/sitecustomize.py를 설치했다:

    import sys
    sys.setdefaultencoding("utf-8")
    

파이썬의 유니코드 지원에 관한 더 자세한 정보는 프로그래머를 위한 유니코드 또는 파이썬으로 만드는 종단대 종단 웹 어플리케이션을 참조하자. 파이썬 요리책 Recipes 1.20 그리고 1.21도 또한 아주 도움이 된다.

기억하자. 터미널 화면이 ASCII로 제한되어 있어도, UTF-8이나 기타 인코딩으로된 문서를 여전히 뷰티플수프를 사용하여 해석하고 처리하고 작성할 수 있다. 단지 특정 문자열은 print로 인쇄할 수 없을 뿐이다.

내가 먹인 데이터를 뷰티플수프가 잃어 버립니다. 왜 그런가요?

뷰티플수프는 구조가-엉성한 SGML을 처리할 수 있지만, 전혀 SGML이 아닌 문서인 경우를 만나면 데이터를 잃어 버리는 경우도 있다. 이는 구조가-엉성한 조판에 비하면 거의 일어나는 일이 아니지만, 웹 크롤러 등등을 구축하려면 반드시 만나게 될 것이다.

유일한 해결책은 미리 데이터를 정규 표현식으로 정돈하는 것이다. 다음은 몇가지 예이다.:

BeautifulSoup가 구축한 해석트리가 마음에 안 들어요!

조판을 다르게 해석하고 싶으면, 다음 기타 내장 해석기나, 그렇지 않으면 맞춤 해석기 구축하기를 참조하자.

뷰티플수프가 너무 느려요!

뷰티플수프는 ElementTree나 맞춤-제작된 SGMLParser 하부클래스만큼 빨리 실행되지 않는다. ElementTree는 C로 작성되었고, SGMLParser로는 오직 원하는 일만 하도록 만든 미니-뷰티플수프를 만들 수 있다. 뷰티플수프의 목적은 처리 시간이 아니라, 프로그래머의 시간을 절약해 주는 것이다.

그렇지만, 뷰티플수프의 속도는 문서에서 오직 필요한 부분만 해석하고, extract를 사용하여 필요하지 않은 객체들을 쓰레기 수집되도록 버리면 상당정도로 높일 수 있다.

고급 주제들

지금까지는 뷰티플수프의 기본적인 사용법을 다루었다. 그러나 HTML과 XML은 까다롭다. 실제 세계에서는 훨씬 더 까다롭다. 그래서 뷰티플수프는 따로 처리 트릭을 가지고 있다.

발생자(Generators)

위에 기술한 검색 메쏘드들은 발생자 메쏘드가 주도한다. 다음 메쏘드를 직접 사용해도 된다: 이른 바 nextGenerator, previousGenerator, nextSiblingGenerator, previousSiblingGenerator, 그리고 parentGenerator가 그것이다. Tag와 해석기 객체도 childGenerator recursiveChildGenerator 메쏘드가 있다.

다음은 문서에서 HTML 태그를 걷어내는 간단한 예이다. 문서를 반복하면서 모든 문자열을 수집함으로써 말이다.

from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup("""<div>You <i>bet</i>
<a href="http://www.crummy.com/software/BeautifulSoup/">BeautifulSoup</a>
rocks!</div>""")

''.join([e for e in soup.recursiveChildGenerator() 
         if isinstance(e,unicode)])
# u'You bet\nBeautifulSoup\nrocks!'

다음은 좀 더 복잡한 예이다. recursiveChildGenerator를 사용하여 문서의 요소들을 반복하면서, 각 요소를 얻자마자 인쇄한다.

from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup("1<a>2<b>3")
g = soup.recursiveChildGenerator()
while True:
    try:
        print g.next()
    except StopIteration:
        break
# 1
# <a>2<b>3</b></a>
# 2
# <b>3</b>
# 3

기타 내장 해석기

뷰티플수프에는 BeautifulSoup 그리고 BeautifulStoneSoup 말고도 세가지 해석기 클래스가 따라온다.:

해석기 재단하기

내장 해석기 객체가 제 일을 하지 않으려고 할 때는 재단할 필요가 있다. 이는 보통 내포화 태그와 단독-닫기 태그 리스트를 맞춤재단한다는 뜻이다. 단독-닫기 태그 리스트를 재단하려면 selfClosingTags 인자를 수프 구성자에 건네면 된다. 그렇지만, 내포화 태그 리스트를 재단하려면, 하부클래스화할 필요가 있다.

하부클래화에 가장 유용한 클래스는 (HTML에) MinimalSoup이고 (XML)에 BeautifulStoneSoup이다. 하부클래스에서 RESET_NESTING_TAGSNESTABLE_TAGS를 오버라이드하는 법을 보여주겠다. 다음은 뷰티플수프에서 가장 복잡한 부분이다. 여기에서 자세히 설명하지는 않겠지만, 대충 써 놓고 나중에 되먹임으로 개선할 생각이다.

뷰티플수프가 문서를 해석할 때는 열린 태그를 스택으로 유지한다. 새로 시작 태그를 볼 때마다, 그 태그를 스택의 위에 올려 놓는다. 그러나 올려놓기 전에, 열린 태그중에서 어떤 것은 닫고 스택에서 제거해야 할 수도 있다. 어느 태그를 닫는지는 발견한 태그의 질과 스택에 있는 태그의 질에 달려 있다.

이를 설명하는 가장 좋은 방법은 예제로 보여주는 것이다. 스택이 ['html', 'p', 'b']과 같과 뷰티플수프가 <P>를 만났다고 생각해 보자. 그냥 또다른 'p' 태그를 스택 위에 올리면, 이는 두 번째 <P> 태그가 첫 <P> 태그 안에 있다는 뜻이된다. 열린 <B> 태그는 말할 것도 없고 말이다. 그러나 그것은 <P> 태그의 작동방식이 아니다. <P> 태그 안에 또다른 <P> 태그를 끼워 넣을 수는 없다. <P> 태그는 내포가 불가능하다.

그래서 뷰티플수프가 <P> 태그를 만나면, 앞에서 만난 같은 유형의 태그를 모두 닫고 꺼낸다. 이것이 기본 행위이다. 그리고 이것이 BeautifulStoneSoup모든 태그를 취급하는 방식이다. 한 태그가 NESTABLE_TAGS이나RESET_NESTING_TAGS에 언급되어 있지 않을 경우와, 또 한 태그가 RESET_NESTING_TAGS에는 나타나지만 NESTABLE_TAGS에 들어있지 않을 경우에 그렇다. <P> 태그의 행위처럼 말이다.

from BeautifulSoup import BeautifulSoup
BeautifulSoup.RESET_NESTING_TAGS['p'] == None
# True
BeautifulSoup.NESTABLE_TAGS.has_key('p')
# False

print BeautifulSoup("<html><p>Para<b>one<p>Para two")
# <html><p>Para<b>one</b></p><p>Para two</p></html>
#                      ^---^--The second <p> tag made those two tags get closed

스택이 ['html', 'span', 'b']와 같고, 뷰티플수프가 <SPAN> 태그를 만났다고 해보자. 이제, <SPAN> 태그에는 제한없이 다른 <SPAN> 태그가 담길 수 있다. 그래서 또 <SPAN> 태그를 만나더라도 앞의 <SPAN> 태그까지 꺼낼 필요는 없다. <SPAN> 태그 이름을 빈 리스트에 짝지워서 NESTABLE_TAGS에 이것을 표현한다. 이런 종류의 태그는 RESET_NESTING_TAGS에 나타나면 안된다: 어떤 상황에서도 <SPAN> 태그 때문에 다른 태그를 꺼내야할 경우는 없다.

from BeautifulSoup import BeautifulSoup
BeautifulSoup.NESTABLE_TAGS['span']
# []
BeautifulSoup.RESET_NESTING_TAGS.has_key('span')
# False

print BeautifulSoup("<html><span>Span<b>one<span>Span two")
# <html><span>Span<b>one<span>Span two</span></b></span></html>

세 번째 예제이다: 스택이['ol','li','ul']와 같다고 해보자: 다시 말해, 순서있는 리스트이며, 첫 요소에는 순서없는 리스트가 들어있다. 이제 뷰티플수프가 <LI> 태그를 만났다고 해 보자. 첫 <LI> 태그까지 꺼낼 필요가 없는데, 왜냐하면 이 새 <LI> 태그는 순서없는 하부리스트(ul)에 포함되기 때문이다. <UL>이나 <OL> 태그가 사이에 있는 한, <LI> 태그가 또다른 <LI> 태그 안에 있는 것은 괞찬다.

from BeautifulSoup import BeautifulSoup
print BeautifulSoup("<ol><li>1<ul><li>A").prettify()
# <ol>
#  <li>
#   1
#   <ul>
#    <li>
#     A
#    </li>
#   </ul>
#  </li>
# </ol>

그러나 사이에 <UL>이나 <OL>이 없다면, 하나의 <LI> 태그는 또다른 <LI> 아래에 있을 수 없다:

print BeautifulSoup("<ol><li>1<li>A").prettify()
# <ol>
#  <li>
#   1
#  </li>
#  <li>
#   A
#  </li>
# </ol>

뷰티플수프에게 <LI> 태그를 이렇게 처리하도록 명령한다. "li"를 RESET_NESTING_TAGS에 넣고, "li"에게 NESTABLE_TAGS 엔트리를 건네준다. 이 엔트리에 내포가 가능한 태그 리스트를 보여준다.

BeautifulSoup.RESET_NESTING_TAGS.has_key('li')
# True
BeautifulSoup.NESTABLE_TAGS['li']
# ['ul', 'ol']

다음도 역시 테이블 태그의 내포를 처리하는 방법이다:

BeautifulSoup.NESTABLE_TAGS['td']
# ['tr']
BeautifulSoup.NESTABLE_TAGS['tr']
# ['table', 'tbody', 'tfoot', 'thead']
BeautifulSoup.NESTABLE_TAGS['tbody']
# ['table']
BeautifulSoup.NESTABLE_TAGS['thead']
# ['table']
BeautifulSoup.NESTABLE_TAGS['tfoot']
# ['table']
BeautifulSoup.NESTABLE_TAGS['table']
# []

다시 말해서: <TD> 태그는 <TR> 태그 안에 내포될 수 있다. <TR> 태그는 <TABLE>, <TBODY>, <TFOOT>, 그리고 <THEAD> 태그 안에 내포될 수 있다. <TBODY>, <TFOOT>, 그리고 <THEAD> 태그는 <TABLE> 태그 안에 내포될 수 있다. 그리고 <TABLE> 태그는 다른 <TABLE> 태그 안에 내포될 수 있다. HTML 테이블에 관하여 알고 있다면, 이런 규칙들은 이미 이해하고 있을 것이다.

한가지 예를 더 들어보자. 스택이 ['html', 'p', 'table']과 같고 뷰티플수프가 <P> 태그를 맞이하면,

언뜻 보면, 이는 마치 스택이 ['html', 'p', 'b']이고 뷰티플수프가 <P> tag인 예제처럼 보인다. 예제에서는 <B>와 <P> 태그를 닫았다. 왜냐하면 한 문단 안에 또 다른 문단을 가질 수 없기 때문이다.

단지... 한 문단 안에 표가 있을 수 있고, 그 표 안에 한 문단이 있을 수 있다. 그래서 올바르게 처리하는 방법은 이런 태그는 닫지 않는 것이다. 뷰티플수프는 올바르게 일을 처리한다:

from BeautifulSoup import BeautifulSoup
print BeautifulSoup("<p>Para 1<b><p>Para 2")
# <p>
#  Para 1
#  <b>
#  </b>
# </p>
# <p>
#  Para 2
# </p>

print BeautifulSoup("<p>Para 1<table><p>Para 2").prettify()
# <p>
#  Para 1
#  <table>
#   <p>
#    Para 2
#   </p>
#  </table>
# </p>

차이가 무엇인가? 그 차이는 <TABLE>은 RESET_NESTING_TAGS 안에 있고 <B>는 그렇지 않다는 것이다. RESET_NESTING_TAGS 안에 있는 태그는 밖에 있는 태그에 비해 스택에서 쉽게 튀어 나오지 못한다.

좋다. 모쪼록 이해하셨으리라 생각한다. 다음은 BeautifulSoup 클래스에 대한 NESTABLE_TAGS이다. 이를 HTML에 대한 지식과 잘 결합하면, 표준을 따르지 않는 괴상한 HTML 문서에 대하여, 또 다양한 내포 규칙이 있는 기타 XML 방언들에 대하여 독자적으로 NESTABLE_TAGS를 만들 수 있을 것이다.

from BeautifulSoup import BeautifulSoup
nestKeys = BeautifulSoup.NESTABLE_TAGS.keys()
nestKeys.sort()
for key in nestKeys:
    print "%s: %s" % (key, BeautifulSoup.NESTABLE_TAGS[key])
# bdo: []
# blockquote: []
# center: []
# dd: ['dl']
# del: []
# div: []
# dl: []
# dt: ['dl']
# fieldset: []
# font: []
# ins: []
# li: ['ul', 'ol']
# object: []
# ol: []
# q: []
# span: []
# sub: []
# sup: []
# table: []
# tbody: ['table']
# td: ['tr']
# tfoot: ['table']
# th: ['tr']
# thead: ['table']
# tr: ['table', 'tbody', 'tfoot', 'thead']
# ul: []

다음은 BeautifulSoupRESET_NESTING_TAGS이다. 오로지 키가 중요하다: RESET_NESTING_TAGS는 실제로는 리스트이다. 신속한 무작위 접근을 위하여 사전의 형태로 들어 간다.

from BeautifulSoup import BeautifulSoup
resetKeys = BeautifulSoup.RESET_NESTING_TAGS.keys()
resetKeys.sort()
resetKeys
# ['address', 'blockquote', 'dd', 'del', 'div', 'dl', 'dt', 'fieldset', 
#  'form', 'ins', 'li', 'noscript', 'ol', 'p', 'pre', 'table', 'tbody',
#  'td', 'tfoot', 'th', 'thead', 'tr', 'ul']

어쨌든 하부클래스화 할 것이기 때문에, 이왕 하는 김에 NESTABLE_TAGS를 오버라이드 하는 편이 좋겠다. 단독-닫기 태그 이름을 아무 값에나 짝짓기 한 사전이 바로 그것이다 (RESET_NESTING_TAGS처럼, 실제로는 사전 형태의 리스트이다.). 그러면 (selfClosingTags처럼) 하부클래스를 실체화할 때마다, 구성자에 그 리스트를 건넬 필요가 없을 것이다.

개체 변환(Entity Conversion)

문서를 해석할 때, HTML이나 XML 개체 참조를 그에 상응하는 유니코드 문자로 변환할 수 있다. 다음 코드는 HTML 개체인 "&eacute;"를 유니코드 문자인 LATIN SMALL LETTER E WITH ACUTE로 변환하며, 수치 개체 "&#101;"을 유니코든 문자인 LATIN SMALL LETTER E로 변환한다.

from BeautifulSoup import BeautifulStoneSoup
BeautifulStoneSoup("Sacr&eacute; bl&#101;u!", 
                   convertEntities=BeautifulStoneSoup.HTML_ENTITIES).contents[0]
# u'Sacr\xe9 bleu!'

이것은 HTML_ENTITIES를 사용할 때이다 (그냥 문자열로 "html"임). XML_ENTITIES(또는 문자열 "xml")을 사용하면, 오직 수치 개체와 다섯개의 XML 개체들 ("&quot;", "&apos;", "&gt;", "&lt;", 그리고 "&amp;")만 변환된다. ALL_ENTITIES (또는 리스트 ["xml", "html"])을 사용하면, 두 종류의 개체 모두 변환될 것이다. 이 마지막 개체가 꼭 필요한데 그 이유는 &apos;가 HTML 개체가 아니라 XML 개체이기 때문이다.

BeautifulStoneSoup("Sacr&eacute; bl&#101;u!", 
                   convertEntities=BeautifulStoneSoup.XML_ENTITIES)
# Sacr&eacute; bleu!

from BeautifulSoup import BeautifulStoneSoup
BeautifulStoneSoup("Il a dit, &lt;&lt;Sacr&eacute; bl&#101;u!&gt;&gt;", 
                   convertEntities=BeautifulStoneSoup.XML_ENTITIES)
# Il a dit, <<Sacr&eacute; bleu!>>

뷰티플수프에게 XML이나 HTML 개체들을 그에 상응하는 유니코드 문자로 변환시키라고 명령하면, (마이크로소프트사의 지능형 따옴표 같은) Windows-1252 문자도 역시 유니코드 문자로 변환된다. 뷰티플수프에게 그런 문자들을 개체로 변환시키라고 명령하더라도 똑 같이 이렇게 처리한다.

from BeautifulSoup import BeautifulStoneSoup
smartQuotesAndEntities = "Il a dit, \x8BSacr&eacute; bl&#101;u!\x9b"

BeautifulStoneSoup(smartQuotesAndEntities, smartQuotesTo="html").contents[0]
# u'Il a dit, &lsaquo;Sacr&eacute; bl&#101;u!&rsaquo;'

BeautifulStoneSoup(smartQuotesAndEntities, convertEntities="html", 
                   smartQuotesTo="html").contents[0]
# u'Il a dit, \u2039Sacr\xe9 bleu!\u203a'

BeautifulStoneSoup(smartQuotesAndEntities, convertEntities="xml", 
                   smartQuotesTo="xml").contents[0]
# u'Il a dit, \u2039Sacr&eacute; bleu!\u203a'

기존의 개체들을 모두 유니코드 문자로 변환하느라 바쁘면서 새로 HTML/XML 개체를 만드는 것은 말이 안된다.

나쁜 데이터를 Regexps로 소독하기

뷰티플수프는 나쁜 조판을 아주 잘 다룬다. 이 때 "나쁜 조판(bad markup)"이란 위치가 잘못된 태그라는 뜻이다. 그러나 어떤 경우 그 조판은 모양을 갖추지 못한 경우도 있고, 그런 경우는 처리하지 못한다. 그래서 뷰티플수프는 입력 문서에 먼저 정규 표현식을 실행한 다음에 해석을 시도한다.

기본값으로, 뷰티플수프는 입력 문서에 대하여 정규 표현식과 교체 함수를 사용하여 검색과-교체를 한다. <BR/> 같은 단독 닫기 태그를 발견하면, 그것을 <BR />와 같이 바꾼다; 선언에서 <! --Comment-->와 같이 관계없는 공백문자를 발견하면, 그 공백문자를 제거한다: <!--Comment-->와 같이 말이다.

어떻게든 고쳐야 할 나쁜 조판이 있다면, 수프 구성자에 자신만의 (정규 표현식, 교체 함수) 터플을 markupMassage 인자로 건네면 된다.

예를 들어 보자: 한 페이지에 모양을 갖추지 못한 주석이 있다고 해 보자. 아래의 SGML 해석기는 이를 처리하지 못하고, 그 주석과 그 이후의 모든 것을 무시한다:

from BeautifulSoup import BeautifulSoup
badString = "Foo<!-This comment is malformed.-->Bar<br/>Baz"
BeautifulSoup(badString)
# Foo

정규 표현식과 함수로 교정해 보자:

import re
myMassage = [(re.compile('<!-([^-])'), lambda match: '<!--' + match.group(1))]
BeautifulSoup(badString, markupMassage=myMassage)
# Foo<!--This comment is malformed.-->Bar

이런, 여전히 <BR> 태그가 빠졌다. markupMassage 함수는 해석기의 기본 메시지를 오버라이드한다. 그래서 기본 검색과-교체 함수는 실행되지 않는다. 해석기는 주석을 지나는 것은 성공하지만, 모양이 나쁜 단독-닫기 태그에서는 죽는다. 새로운 메시지 함수를 기본 리스트에 추가해 보자. 그래서 모든 함수들이 실행되게 말이다.

import copy
myNewMassage = copy.copy(BeautifulSoup.MARKUP_MASSAGE)
myNewMassage.extend(myMassage)
BeautifulSoup(badString, markupMassage=myNewMassage)
# Foo<!--This comment is malformed.-->Bar<br />Baz

이제 모두 성공했다.

조판에 정규 표현식이 있어야 할 필요가 없다는 사실을 알고 있다면, 그냥 markupMassage에 대하여 False 를 건네면 더 빠르게 기동할 수 있다.

재미있는 SoupStrainer

검색 메쏘드들은 거의 비슷한 인자들을 취한다는 사실을 기억하자. 배경 뒤에서, 메쏘드에 건네어진 인자들은 SoupStrainer 객체로 변환된다. (findAll 같이) 리스트를 돌려주는 메쏘드 중의 하나를 호출하면, SoupStrainer 객체는 결과 리스트에서 source 특성으로 얻을 수 있다.

from BeautifulSoup import BeautifulStoneSoup
xml = '<person name="Bob"><parent rel="mother" name="Alice">'
xmlSoup = BeautifulStoneSoup(xml)
results = xmlSoup.findAll(rel='mother')

results.source
# <BeautifulSoup.SoupStrainer instance at 0xb7e0158c>
str(results.source)
# "None|{'rel': 'mother'}"

SoupStrainer 구성자는 find와 같은 인자를 취한다: name, attrs, text, 그리고 **kwargs가 그것이다. SoupStrainername 인자로 검색 메쏘드에 건넬 수 있다:

xmlSoup.findAll(results.source) == results
# True

customStrainer = BeautifulSoup.SoupStrainer(rel='mother')
xmlSoup.findAll(customStrainer) == results
#  True

뭐, 누가 신경쓰겠는가? 메쏘드 호출에 건네는 인자는 다른 방식으로도 얼마든지 건넬 수 있다. 그러나 SoupStrainer로 할 수 있는 또다른 일은 그것을 수프 구성자에게 건네서 문서의 일부만 실제로 해석되도록 제한하는 것이다. 이것이 다음 섹션의 주제이다:

문서의 일부만 해석해서 수행성능 개선하기

뷰티플수프는 문서 안의 모든 요소들을 파이썬 객체로 바꾸고 그 객체를 다른 파이썬 객체 다발에 붙인다. 문서의 일부만 필요하면, 이는 정말 느리다. 그러나 SoupStrainerparseOnlyThese 인자로 수프 구성자에 건넬 수 있다. 뷰티플수프는 각 요소를 SoupStrainer에 대하여 점검하고, 부합할 경우에만 그 요소는 TagNavigableText로 바뀌어서, 트리에 추가된다.

트리에 한 요소가 추가되면, 그의 자손들도 역시 추가된다. 자신의 SoupStrainer에 부합하지 않더라도 말이다. 이렇게 하면 문서에서 필요한 데이터를 담고 있는 부분만 해석할 수 있다.

다음은 아주 번잡한 문서이다:

doc = '''Bob reports <a href="http://www.bob.com/">success</a>
with his plasma breeding <a
href="http://www.bob.com/plasma">experiments</a>. <i>Don't get any on
us, Bob!</i>

<br><br>Ever hear of annular fusion? The folks at <a
href="http://www.boogabooga.net/">BoogaBooga</a> sure seem obsessed
with it. Secret project, or <b>WEB MADNESS?</b> You decide!'''

다음은 원하는 부분에 따라 문서를 수프로 해석하는 여러 다양한 방법이다. 이 모든 방법은 전체 문서를 해석한 다음 똑 같은 SoupStrainer를 사용하여 원하는 부분들을 골라내는 방법보다 더 빠르고 메모리도 덜 사용한다.

from BeautifulSoup import BeautifulSoup, SoupStrainer
import re

links = SoupStrainer('a')
[tag for tag in BeautifulSoup(doc, parseOnlyThese=links)]
# [<a href="http://www.bob.com/">success</a>, 
#  <a href="http://www.bob.com/plasma">experiments</a>, 
#  <a href="http://www.boogabooga.net/">BoogaBooga</a>]

linksToBob = SoupStrainer('a', href=re.compile('bob.com/'))
[tag for tag in BeautifulSoup(doc, parseOnlyThese=linksToBob)]
# [<a href="http://www.bob.com/">success</a>, 
#  <a href="http://www.bob.com/plasma">experiments</a>]

mentionsOfBob = SoupStrainer(text=re.compile("Bob"))
[text for text in BeautifulSoup(doc, parseOnlyThese=mentionsOfBob)]
# [u'Bob reports ', u"Don't get any on\nus, Bob!"]

allCaps = SoupStrainer(text=lambda(t):t.upper()==t)
[text for text in BeautifulSoup(doc, parseOnlyThese=allCaps)]
# [u'. ', u'\n', u'WEB MADNESS?']

검색 메쏘드에 건넨 SoupStrainer와 수프 구성자에 건넨 SoupStrainer 사이에는 한가지 큰 차이가 있다. 기억을 되살려보자. name 인자는 인자가 Tag 객체인 함수를 취할 수 있다. SoupStrainername에 대해서는 이렇게 할 수 없다. 왜냐하면, SoupStrainer는 먼저 Tag 객체가 생성되어야 하는지 결정하는데 사용되기 때문이다. SoupStrainername에 함수를 건넬 수 있지만, Tag 객체를 받지 않을 것이다: 오직 태그 이름과 인자 맵만 취할 수 있다.

shortWithNoAttrs = SoupStrainer(lambda name, attrs: \
                                len(name) == 1 and not attrs)
[tag for tag in BeautifulSoup(doc, parseOnlyThese=shortWithNoAttrs)]
# [<i>Don't get any on us, Bob!</i>, 
#  <b>WEB MADNESS?</b>]

extract로 메모리 사용법 개선하기

뷰티플수프가 문서를 해석할 때는 긴밀하게 연결된 거대 데이터 구조를 한꺼번에 모두 메모리에 적재한다. 그 데이터 구조에서 문자열 하나만 필요할 뿐이라면, 그 문자열만 얻고 나머지는 쓰레기 처리되도록 두었으면 하고 생각할지 모르겠다. 그렇게는 안 된다. 그 문자열은 NavigableString 객체이다. 그것은 parent 멤버가 있고 이 멤버는 Tag 객체를 가리킨다. 이 객체는 다른 Tag 객체를 가리킨다, 등등. 트리의 일부를 가지고 있는 한, 전체를 모두 메모리에 가지고 있는 셈이다.

extract 메쏘드는 그런 연결고리를 끊는다. 필요한 문자열에다 extract를 호출하면, 나머지 해석 트리와 연결고리가 끊긴다. 나머지 트리는 영역을 벗어나 쓰레기 처리될 것이다. 다른 일에 그 문자열을 사용하는 동안에 말이다. 해석 트리에서 일부만 필요할 뿐이라면, extract를 그의 최-상위 Tag에 호출하면 된다. 나머지 해석트리는 쓰레기 처리될 것이다.

이것도 역시 다른 방식으로 작동한다. 문서에서 상당 부분이 필요하지 않다면, extract를 호출하여 그 부분을 트리에서 걷어낸 다음, 나머지 (작은) 트리를 제어하는 동안에 쓰레기 처리되도록 버리면 된다.

해석 트리에서 많은 부분을 버릴 생각이라면, 처음부터 해석트리의 일부를 해석하지 않으면 시간을 절약할 수 있을 것이다.

다음도 참조

이것 저것

뷰티플수프를 사용하는 어플리케이션

실세계에서 뷰티플수프를 사용하는 어플리케이션이 많다. 다음은 공개된 어플리케이션이다:

비슷한 라이브러리들

여러분 대신 트리를 순회하여 주거나 나쁜 조판을 처리할 수 있는 또는 보통의 해석기보다 더 유용한 해석기들이 다양한 언어로 나와 있다.

맺는 말

이상이다! 즐거운 시간 되시기를! 뷰티플수프로 누구든지 시간을 절약하시기를 바란다. 일단 익숙해지면, 형편없이 설계된 웹 사이트라도 단 몇분이면 데이터를 추출하실 수 있으리라 믿는다. 조언이 있거나 문제가 있거나 또는 뷰티플수프를 사용하는 프로젝트를 알려주고 싶으신 분들은 이메일을 보내주시기를 바란다.

--레오나르도(Leonard) 알림


(이 문서는) 레오나르도 리차드슨(Leonard Richardson)의 웹공간인, Crummy의 일부이다 (연락 정보). 마지막으로 수정된 것은 July 26 2006, 16:26:30 Nowhere Daylight Time 이며 그리고 마지막으로 구축된 때는 Tuesday, February 06 2007, 23:01:09 Nowhere Standard Time이다.

Crummy 사이트는 ⓒ 1996-2007 Leonard Richardson의 소유이다. 따로 고지하지 않는 한, 모든 텍스트는 창조적 공공재 라이센스(Creative Commons License)를 따른다.

문서 트리:

http://www.crummy.com/
software/
BeautifulSoup/
documentation.html
사이트 검색: