| 저자: | 데이브 쿨만(Dave Kuhlman) |
|---|---|
| 한글판: | johnsonj 2007.01.16 원문위치 |
| Address: | dkuhlman@rexx.com http://www.rexx.com/~dkuhlman |
| Revision: | 1.0a |
| Date: | Jan. 9, 2004 |
| Copyright: | Copyright (c) 2004 Dave Kuhlman. 이 문서는 MIT 라이센스로 보호된다: http://www.opensource.org/licenses/mit-license. |
요약
이 문서에서는 HTML 화면 긁어모으기를 설명한다. 실제로 웹을 한 자원으로 다루는 법을 보여준다. HTML 웹 페이지로부터 데이터를 추출하여 열람하는 법을 배울 수 있다.
웹에는 엄청난 양의 정보가 있다. 이 문서는 퀴호테 웹 어플리케이션 뒤의 후방 자원으로 웹을 사용하는 법을 보여준다.
이 문서는 퀴호테 어플리케이션 뒤에서 이렇게 하는 방법을 설명한다 (그렇지만 비슷한 테크닉으로 다른 환경에서도 사용이 가능하다). 특히, 다음을 살펴보겠다:
배포 파일에 소스 코드가 들어 있다. 이 파일에서 예제들을 뽑았다. 다음에서 찾을 수 있다: http://www.rexx.com/~dkuhlman/quixote_htmlscraping.zip.
퀴호테 어플리케이션을 위한 후방-자원으로서의 웹은 그냥 퀴호테 어플리케이션 뒤에 놓을 수 있는 자원중의 하나일 뿐이다. 또다른 후방 자원으로, 예를 들면 관계형 데이터베이스와 XML-RPC 그리고 SOAP와 파이썬 확장 모듈 등이 있다. 퀴호테 아래에서 여러 후방 자원에 접근하는 법에 대한 정보를 더 자세하게 알고 싶다면, 퀴호테(Quixote) 어플리케이션: 시작하기에서 특수 작업 -- 후방 자원을 참조하자 .
명령어 줄에서 테스트 패턴을 실행해서 빨리 만들면 좋은 경우가 있다. 다음이 바로 그렇게 하는 간단한 함수이다. urllib 모듈을 사용하여 웹 페이지를 열람하고 popen2 모듈을 사용하여 sgrep을 실행한다:
import urllib
import popen2
#
# URL의 소를 훓어본다.
# URL을 열람한 다음, 그 내용(문자열)을 sgrep에 먹인다.
#
def search_url(pattern, url, addOptions):
if not addOptions:
addOptions = ''
options = "-g html -o '%r:::' -T " + addOptions
cmd = "sgrep %s '%s' -" % (options, pattern)
print 'cmd:', cmd
try:
instream = urllib.urlopen(url)
except IOError:
print '*** bad url: %s' % url
return
content = instream.read()
instream.close()
print 'len(content): %d' % len(content)
# 내용을 sgrep에 먹이고 그 결과를 수집한다.
outfile, infile = popen2.popen2(cmd)
infile.write(content)
infile.close()
results = outfile.read()
outfile.close()
print 'results:\n========\n'
resultlist = results.split(':::')
for result in resultlist:
if result.strip():
print result
print '---------------'
설명:
솔직히 말해 퀴호테 안에서 이렇게 하는 일은 기본적으로 퀴호테 밖에서 하는 것과 똑 같다. 역시 또, urllib를 사용하여 웹 페이지를 열람한 다음, sgrep을 사용하여 그 페이지로부터 텍스트 조각들을 추출하고, 마지막으로 파이썬의 정규 표현식 모듈 re (또는 기타 파이썬 해석 테크닉)을 사용하여 데이터 항목들을 sgrep이 돌려주는 결과로부터 추출한다.
한가지 걱정은 지연시간이다. 특히, 다음이 문제가 될 수 있다:
그런 걱정을 불식시키기 위하여 다음을 시도해 볼만하다:
다음 섹션은 HTML 문서 안에서 데이터를 선택하는 sgrep 패턴을 작성하는데 도움이 된다. 수행하고 싶어할 만한 sgrep으로 전형적인 데이터 추출 작업을 하기 위한 예제들이 포함되어 있다.
몇가지 예제들:
"HREF" 속성에 값이 "python"인 요소들을 추출한다:
elements parenting (attribute("HREF") containing "python")
값이 "python"인 "HREF" 속성에 대하여 값을 추출한다:
attribute("HREF") containing attvalue("*")
"python"을 값으로 하는 "HREF" 속성을 담고 있는 요소의 내용을 추출한다:
stag("A") containing (attribute("HREF") containing "python") __ etag("A")
"__" (두개의 밑줄문자) 연산자가 non-inclusive 지역을 선택한다는 것에 주목하자. 이 경우, 시작 태그와 끝 태그를 포함하지 않는 지역을 선택한다. 반면, ".." (두개의 점문자) 연산자는 inclusive 지역을 선택한다. "__"를 ".."으로 교체하면 데이터 내용 뿐만 아니라 그를 둘러싼 "A" 태그까지 반환된다.
모든 "HREF" 속성들에 대하여 값을 추출한다:
attvalue("*") in attribute("HREF")
"TD" 요소를 담고 있는 모든 "TR" 요소를 추출한다. 이 "TD" 요소는 "A" 요소를 간접적으로 담고 있다.
(stag("TR") .. etag("TR")) containing (stag("TD") .. etag("TD")) parenting (stag("A") .. etag("A"))
그러나, 하나 이상의 "parenting" 연산자를 사용하면 어떤 문제점이 있는지 주의사항을 아래에서 읽어보자.
몇 가지 추가 논평과 주의사항:
sgrep을 실행하기 위한 두 가지 테크닉이 있다:
pysgrep의 사용법은 PySgrep - Sgrep을 위한 파이썬 포장자에 기술되어 있으므로, 여기에서 재론하지 않겠다.
popen2을 사용하는 것이 간단하고 아래 예제에서 사용될 것이다.
비교 -- PySgrep vs. popen2:
sgrep은 (아직) 정규 표현식을 사용할 능력을 갖추지 못하였다. 파이썬의 re 모듈을 사용하고 그것을 sgrep이 산출한 결과에 적용하면 정규 표현식의 효과를 얻을 수 있다.
기본적으로 sgrep이 돌려주는 각각의 코드조각으로부터 데이터 조각들을 정규 표현식을 사용하여 추출하겠다.
예제 하나 -- 서버와 도메인 이름을 URL에서 추출하고 싶다고 해 보자. 예를 들어, sgrep이 다음과 같은 것을 돌려준다고 가정하자:
http://www.python.org/doc/current/tut/tut.html
그리고, 다음과 같이 추출하고 싶다:
www.python.org
다음이 그 방법이다:
#
# 파일들을 훓는다.
# 파일들을 읽어서, 파일 내용(문자열)을 sgrep에 먹인다.
#
def search_files(pattern, filenames, addOptions, regex):
if not addOptions:
addOptions = ''
options = "-g html -o '%r:::' " + addOptions
#
# 정규 표현식이 있다면 컴파일 한다.
expr = None
if regex:
expr = re.compile(regex)
cmd = "sgrep %s '%s' -" % (options, pattern)
for filename in filenames:
inputfile = file(filename, 'r')
outfile, infile = popen2.popen2(cmd)
infile.write(inputfile.read())
infile.close()
results = outfile.read()
outfile.close()
print '=' * 50
s1 = 'file: %s' % filename
print s1
print '=' * len(s1)
resultlist = results.split(':::')
for result in resultlist:
if result.strip():
print result
#
# 정규 표현식이 있다면
# 이용하여 결과를 검색한다.
if expr:
matchobject = expr.search(result)
if matchobject:
print 'match: %s' % matchobject.group(1)
else:
print 'no match'
print '---------------'
설명:
이 함수가 정규 표현식을 건네면, 그 정규 표현식을 컴파일하고 그것을 사용하여, sgrep이 돌려주는 각각의 결과를 검색한다.
정규 표현식 검색이 성공하면 (즉, "expr.search(result)"이 일치 객체를 하나 돌려주면), 그 일치 객체로부터 첫 그룹을 열람한다. 정규 표현식에 적어도 그룹이 하나 존재한다고 가정한다 (특히 적어도 하나 이상의 괄호 쌍이 있다고 가정함).
서버와 도메인을 URL로부터 추출하려면, 이 함수에 다음 정규 표현식을 건네야 할 것이다:
'https?://([^/]*)'
파이썬 정규 표현식에 관한 더 자세한 정보는, re -- 정규 표현식 연산을 참조하자.
어떤 경우 sgrep이 돌려주는 테스트 조각에 HTML 조판이 들어 있는 경우가 있다. 좀 복잡해서 정규 표현식으로 분석하려면 곤란하다. 또 어떤 경우는 그냥 sgrep에 HTML 조판을 그대로 돌려달라고 요청하는 것이 더 쉽다. 다음 섹션에서는 파이썬의 HTMLParser 모듈을 사용하여 이런 텍스트 조각들을 분석하는 법을 보여준다.
이런 테크닉은 HTMLParser.feed()에게 "완전한" 조판 조각을 넘겨줄 필요가 있을 경우로 제한된다. 즉 서로 쌍을 이루는 태그를 넘겨줄 필요가 있을 경우에만 사용해야 한다. 그렇지만, 이런 필수조건은 sgrep으로 쉽게 만족시킬 수 있다. 왜냐하면, "(stag("tag") .. etag("tag")) parenting ..."와 같은 형태의 질의가 돌려주는 HTML 조판 조각을 HTMLParser.feed(data) 메쏘드에 먹이면 된다. 그리고, 단 한번의 호출로 조판 조각들을 모두 다 HTMLParser.feed(data)에 먹일 필요가 없음을 주목하자; 여러번 feed를 호출해서 그렇게 해도 된다.
다음은 상당히 간단한 예제이다. 이 예제는 질의 하나를 가지고 http://jobs.com/를 검색한 다음, (1) 간략한 구인 정보, (2) URL 하나, (3) 회사 이름, 그리고 (4) 직장 위치를 추출하고 나서, 이렇게 추출된 정보로 웹 페이지 하나를 포맷한다.
다음은 질의와 데이터 추출을 수행하는 코드이다:
class JobService:
#
# 질의 하나를 처리한다.
# 터플을 돌려준다: (urlList, descriptionList, companyList, locationList).
# 질의는 단어의 연속열로서 공백문자로 분리된다.
#
def job_search(self, query):
if query:
q1 = query.replace(' ', '.')
q2 = query.replace(' ', '%26')
q3 = query.replace(' ', '+')
else:
return [], [], [], []
req = 'http://%s.jobs.com/jobsearch.asp?re=9&vw=b&pg=1&cy=US&sq=%s&aj=%s' \
% (q1, q2, q3)
f = urllib.urlopen(req)
content = f.read()
f.close()
resultTuple = self.job_parse(content)
return resultTuple
def job_parse(self, content):
# URL을 추출한다.
cmd = "sgrep -g html -o '%r:::' 'attribute(\"HREF\") in " \
"((stag(\"A\") .. etag(\"A\")) childrening " \
"(stag(\"TD\") .. etag(\"TD\")) containing " \
"attribute(\"HREF\") containing \"getjob\")' -"
urlList = self.extract(cmd, content)
# 설명을 추출한다.
cmd = "sgrep -g html -o '%r:::' '(stag(\"A\") __ etag(\"A\")) in " \
"((stag(\"A\") .. etag(\"A\")) childrening " \
"(stag(\"TD\") .. etag(\"TD\")) containing " \
"attribute(\"HREF\") containing \"getjob\")' -"
descriptionList = self.extract(cmd, content)
# 회사 이름과 위치를 추출한다.
cmd = "sgrep -g html -o '%r:::' '(stag(\"TR\") .. etag(\"TR\")) containing " \
"(stag(\"TD\") .. etag(\"TD\")) parenting " \
"stag(\"A\") containing " \
"attribute(\"HREF\") containing \"getjob\"'"
companyList, locationList = self.extract_with_htmlparser(cmd, content)
return urlList, descriptionList, companyList, locationList
def extract(self, cmd, content):
outfile, infile = popen2.popen2(cmd)
infile.write(content)
infile.close()
results = outfile.read()
outfile.close()
resultList = results.split(':::')
return resultList
def extract_with_htmlparser(self, cmd, content):
parser = LocationHTMLParser()
outfile, infile = popen2.popen2(cmd)
infile.write(content)
infile.close()
results = outfile.read()
outfile.close()
resultList = results.split(':::')
companyList = []
locationList = []
for result in resultList:
parser.clear()
parser.feed(result)
companyList.append(parser.getCompany())
locationList.append(parser.getLocation())
return companyList, locationList
class LocationHTMLParser(HTMLParser.HTMLParser):
def __init__(self):
HTMLParser.HTMLParser.__init__(self)
self.count = 0
self.company = ''
self.location = ''
def handle_starttag(self, tag, attrs):
if tag == 'td':
self.count += 1
## def handle_endtag(self, tag):
## pass
def handle_data(self, data):
if self.count == 4:
self.company += data
elif self.count == 5:
self.location += data
#
# self에 주목: "reset"이라는 이름을 사용하지 말 것.
# HTMLParser가 그 이름을 정의하고 사용한다.
def clear(self):
self.count = 0
self.company = ''
self.location = ''
def getCompany(self):
return self.company
def getLocation(self):
return self.location
설명:
다음은 퀴호테 웹 사용자 인터페이스를 제공하고 웹 페이지를 생성하는 코드이다:
class ServicesUI:
o
o
o
def do_job_search [html] (self, request):
queryString = widget.StringWidget('query_string')
submit = widget.SubmitButtonWidget(value='Search')
if request.form:
queryStringValue = queryString.parse(request)
else:
queryStringValue = ''
jobservice = services.JobService()
urlList, descriptionList, companyList, locationList = jobservice.job_search(
str(queryStringValue))
header('Jobs')
'<form method="POST" action="job_search">\n'
'<p>Query string:'
queryString.render(request)
'</p>\n'
'<p>'
submit.render(request)
'</p>\n'
'</form>\n'
'<hr/>\n'
'<ul>\n'
re1 = re.compile(str('href="([^"]*)"'))
q1 = queryStringValue.replace(str(' '), str('.'))
for idx in range(len(urlList)):
result = urlList[idx]
description = descriptionList[idx]
company = companyList[idx]
location = locationList[idx]
if result.strip():
mo = re1.search(result)
if mo:
url = mo.group(1)
'<li><a href="http://%s.jobs.com%s">%s</a> at %s in %s</li>' % \
(q1, url, description, company, location)
'</ul>'
footer()
설명:
다음 섹션은 간단하게 요약/검토하고 제안을 하면서 다음과 같은 일들을 차례대로 따라가 보겠다.
각 HTML 긁어 모으기 연산에 대하여, 다음과 같은 일을 한다:
URL을 결정하고 잡아온다 -- 웹 브라우저를 사용하여 원하는 데이터가 있는 페이지를 방문한다. 다음 그 주소의 내용을 복사한다. 파이썬 코드에서는 URL에 인자를 교체할 필요가 있을 수 있으니 주의하자.
샘플 웹 페이지 하나를 파일로 저장한다. 다음은 웹 페이지 하나를 열람하고 그것을 표준화면에 쓰는 간단한 스크립트이다. 마음대로 파일에 파이프 처리해 넣을 수 있다:
import urllib
def get_page(url):
f = urllib.urlopen(url)
content = f.read()
f.close()
print content
파일에 잡아 둔 이 샘플 페이지로부터 데이터 항목들을 추출하고 싶다면, 오프-라인(즉 퀴호테 밖)에서 스크립트를 작성하고 테스트하자. 다음은 데이터 추출 스크립트를 테스트해 보는데 사용할 수 있는 설비이다. 실제로 유닛 테스트에서 취했으며, 위의 예제에 있는 코드를 테스트한다:
from test2.services import jobsearchservices
class TestDataExtraction(unittest.TestCase):
o
o
o
def test_retrieve_and_parse(self):
jobservice = jobsearchservices.JobService()
urlList, descriptionList, companyList, locationList = \
jobservice.job_search('python internet')
self.assert_(len(urlList) > 0)
self.assert_(len(urlList) == len(descriptionList))
self.assert_(len(urlList) == len(companyList))
self.assert_(len(urlList) == len(locationList))
방금 테스트 한 데이터 추출 함수를 퀴호테 어플리케이션 안에 잘라 붙이자. 또는, 약간 준비를 하고, 이 데이터 추출 함수들을 파이썬 모듈 안에 배치해도 좋다. 오프-라인 테스트 동안 그리고 퀴호테 어플리케이션 안에서 사용할 수 있다..
파이썬의 unittest 작업틀을 이용하여 데이터 추출 함수들을 위한 테스트들을 설정하자.
훌륭한 개발 방법은 -- 첫째가, 문서이다. 둘째는, 유닛 테스트이다. 그 다음이 코드이다.
다음은 샘플 유닛 테스트로서 이것으로 시작해 보자:
#!/usr/bin/env python
import unittest
from test2.services import jobsearchservices
class TestDataExtraction(unittest.TestCase):
def setUp(self):
pass
def test_retrieve_and_parse(self):
jobservice = jobsearchservices.JobService()
urlList, descriptionList, companyList, locationList = \
jobservice.job_search('python internet')
self.assert_(len(urlList) > 0)
self.assert_(len(urlList) == len(descriptionList))
self.assert_(len(urlList) == len(companyList))
self.assert_(len(urlList) == len(locationList))
if __name__ == '__main__':
unittest.main()
설명:
다음은 HTML 데이터의 접근을 구현하는데 필요한 몇가지 제안사항이다:
웹의 거대한 자원이다. 빼놓은 것이 있다면 그것을 사용해야 할 동기와 그 도구들이다. 어쨌거나 ...
http://www.mems-exchange.org/software/quixote/: 퀴호테(Quixote) 지원 웹 사이트.
sgrep - 파일에서 구조화된 패턴을 찾는다.: sgrep 매뉴얼.
re -- 정규 표현식 연산: 파이썬의 정규 표현식 모듈에 관한 문서.
PySgrep - Sgrep에 대한 파이썬 포장자: sgrep 파이썬 확장 모듈에 관한 자세한 정보.
unittest: 파이썬 유닛 테스트 작업틀