☜ 제 09 장 XML 처리 """ Dive Into Python """
다이빙 파이썬
제 11 장 HTTP 웹 서비스 ☞

제 10 장 스크립트와 스트림

10.1. 입력 소스의 추상화

파이썬의 위대한 장점중의 하나는 그의 역동적인 바인딩입니다. 역동적 바인딩의 강력한 사용예 중의 하나는 파일-류의 객체입니다.

입력 소스를 요구하는 많은 함수들은 그냥 파일이름을 취하고, 그 파일을 읽기 모드로 열어서 읽은 다음, 일이 끝나면 닫을 수 있습니다. 그러나 그렇게 하지 않고, 대신에 파일-류의 객체를 취합니다.

가장 단순한 형태로 파일-류의 객체는 선택적으로 size 매개변수를 가진 read 메쏘드만 있으면 됩니다. 이 메쏘드는 문자열을 돌려줍니다. size 매개변수 없이 호출하면 입력 소스에서 읽어야 할 것을 모조리 읽어서 단 하나의 문자열로 돌려줍니다. size 매개변수를 가지고 호출하면 그 만큼만 입력 소스로부터 읽어서 그 만큼의 데이터를 돌려줍니다; 다시 또 호출하면 떠났던 바로 그곳에서 시작해서 다음 그 만큼의 데이터를 돌려줍니다.

이 방법은 실제 파일에서 읽는 방법과 같습니다; 차이점이라면 실제 파일에 얽매이지 않는다는 것입니다. 입력 소스는 무엇이든 될 수 있습니다: 디스크에 저장된 파일, 웹 페이지, 심지어 하드-코드된 문자열도 가능합니다. 파일-류의 객체를 함수에 건네는 한, 함수는 그 객체의 read 메쏘드를 호출합니다. 함수는 어떤 종류의 입력 소스도 처리할 수 있습니다. 종류마다 처리할 특정 코드가 없어도 됩니다.

이것이 XML 처리와 어떻게 관련되는지 궁금하실텐데, minidom.parse가 바로 파일-류의 객체를 취하는 그런 함수입니다.

예제 10.1. 파일로부터 XML 해석하는 법

>>> from xml.dom import minidom
>>> fsock = open('binary.xml')    
>>> xmldoc = minidom.parse(fsock) 
>>> fsock.close()                 
>>> print xmldoc.toxml()          
<?xml version="1.0" ?>
<grammar>
<ref id="bit">
  <p>0</p>
  <p>1</p>
</ref>
<ref id="byte">
  <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar>
먼저, 디스크에 저장된 파일을 엽니다. 이렇게 하면 파일 객체를 돌려줍니다.
파일 객체를 minidom.parse에 건넵니다. 이 함수는 fsockread 메쏘드를 요청하고 디스크에 저장된 파일로부터 XML 문서를 읽습니다.
일이 끝나면 잊지 말고 파일 객체에 close 메쏘드를 요청하세요. minidom.parse는 여러분을 대신하여 닫아주지 않습니다.
반환된 XML 문서에 toxml() 메쏘드를 요청하면 모조리 인쇄합니다.

자, 이 모든 것이 시간 낭비처럼 보입니다. 어쨋거나 minidom.parse가 파일이름을 취해서 열고 닫는 귀찮은 작업을 자동으로 해줄 수 있음을 보셨습니다. 사실 그냥 지역 파일을 해석할 생각이라면 파일이름을 건네기만 하면 minidom.parse올바른 일을 해 줄 만큼 똑똑합니다™. 그러나 인터넷으로부터 바로 XML 문서를 받아 해석하는 것이 얼마나 비슷한지 -- 그리고 얼마나 쉬운지 -- 느껴 보세요.

예제 10.2. URL로부터 XML 해석하는 법

>>> import urllib
>>> usock = urllib.urlopen('http://slashdot.org/slashdot.rdf') 
>>> xmldoc = minidom.parse(usock)                              
>>> usock.close()                                              
>>> print xmldoc.toxml()                                       
<?xml version="1.0" ?>
<rdf:RDF xmlns="http://my.netscape.com/rdf/simple/0.9/"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

<channel>
<title>Slashdot</title>
<link>http://slashdot.org/</link>
<description>News for nerds, stuff that matters</description>
</channel>

<image>
<title>Slashdot</title>
<url>http://images.slashdot.org/topics/topicslashdot.gif</url>
<link>http://slashdot.org/</link>
</image>

<item>
<title>To HDTV or Not to HDTV?</title>
<link>http://slashdot.org/article.pl?sid=01/12/28/0421241</link>
</item>

[...snip...]
앞 장에서 보셨듯이, urlopen은 웹 페이지 URL을 취해 파일-류의 객체를 돌려줍니다. 정말 중요한 것은 이 객체에 read 메쏘드가 있어서 그 웹 페이지의 HTML 소스를 돌려준다는 것입니다.
이제 파일-류의 객체를 minidom.parse에 건네면 그 객체의 read 메쏘드를 호출하고 read 메쏘드가 돌려주는 XML 데이터를 파싱합니다. 이제 이 XML 데이터가 웹 페이지로부터 온다는 사실은 완전히 관련이 없습니다. minidom.parse는 웹 페이지에 관하여 알지 못하며, 웹 페이지에 관하여 신경쓰지 않습니다; 단지 파일-류의 객체에 관해서만 알 뿐입니다.
일을 마치자 마자, 잊지 말고 urlopen이 돌려준 파일-류의 객체를 닫으세요.
그런데, 이 URL은 실제이고, 실제로 XML입니다. 기술 뉴스와 소식을 전하는 Slashdot 사이트의 헤드라인을 XML로 표현합니다.

예제 10.3.  문자열로부터 XML 해석하는 법 (쉽지만 유연하지 못한 방법)

>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> xmldoc = minidom.parseString(contents) 
>>> print xmldoc.toxml()
<?xml version="1.0" ?>
<grammar><ref id="bit"><p>0</p><p>1</p></ref></grammar>
minidom에는 parseString이라는 메쏘드가 있습니다. 이 메쏘드는 전체 XML 문서를 문자열로 받아 해석합니다.minidom.parse 대신에 이를 사용할 수 있습니다. 전체 XML 문서를 문자열로 가지고 있다는 사실을 이미 알고 있다면 말입니다.

좋습니다. 그래서 minidom.parse 함수를 사용하면 지역 파일과 원격 URL을 해석할수 있을 뿐만 아니라, 문자열도 해석할 수 있습니다. 다른 함수를... 사용해서 말입니다. 파일이나 URL 또는 문자열로부터 입력을 받고 싶다면 문자열인지 점검하는 특별한 로직이 필요하다는 뜻이고 그러면 대신에 parseString 함수를 호출할 필요가 있다는 있습니다. 짜증나는 일입니다.

문자열을 파일-류의 객체로 바꿀 방법이 있다면 이 객체를 minidom.parse에 건네도 됩니다. 사실, 바로 그런 일을 하기 위해 특별히 설계된 모듈이 있습니다: StringIO가 바로 그것입니다.

예제 10.4. StringIO 소개

>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> import StringIO
>>> ssock = StringIO.StringIO(contents)   
>>> ssock.read()                          
"<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> ssock.read()                          
''
>>> ssock.seek(0)                         
>>> ssock.read(15)                        
'<grammar><ref i'
>>> ssock.read(15)
"d='bit'><p>0</p"
>>> ssock.read()
'><p>1</p></ref></grammar>'
>>> ssock.close()                         
StringIO 모듈에는 클래스가 딱 하나 있습니다. 이른바 StringIO라고 부르는데, 문자열을 파일-류의 객체로 변환할 수 있습니다. StringIO 클래스는 실체를 생성할 때 문자열을 매개변수로 취합니다.
이제 파일-류의 객체가 있고, 그것을 가지고 온갖 종류의 파일-관련 처리를 할 수 있습니다. read처럼 말입니다. 이 메쏘드는 원래의 문자열을 돌려줍니다.
다시 read를 호출하면 빈 문자열을 돌려줍니다. 이는 실제 파일 객체가 작동하는 방식이기도 합니다; 전체 파일을 한번 읽고 나면 더이상 읽을 수 없습니다. 명시적으로 파일의 처음으로 포인터를 되돌리지 않으면 말입니다. StringIO 객체도 똑 같은 방식으로 작동합니다.
전체 파일에 걸쳐서 포인터를 움직이듯이 명시적으로 문자열의 처음으로 갈 수 있습니다. StringIO 객체의 seek 메쏘드를 사용하면 됩니다.
문자열을 조각내서 읽을 수도 있습니다. read 메쏘드에 size 매개변수를 건네면 됩니다.
언제든지, read 메쏘드는 아직 읽지 않은 나머지 문자열을 모두 돌려줍니다. 이 모든 것은 정확하게 파일 객체가 작동하는 방식입니다; 그러므로 파일-류의 객체라는 용어를 사용합니다.

예제 10.5. 문자열로부터 XML 해석하는 법 (파일-류의 객체를 사용하는 법)

>>> contents = "<grammar><ref id='bit'><p>0</p><p>1</p></ref></grammar>"
>>> ssock = StringIO.StringIO(contents)
>>> xmldoc = minidom.parse(ssock) 
>>> ssock.close()
>>> print xmldoc.toxml()
<?xml version="1.0" ?>
<grammar><ref id="bit"><p>0</p><p>1</p></ref></grammar>
이제 파일-류 객체 (실제로는 StringIO)를 minidom.parse에 건넬 수 있습니다. minidom.parse는 그 객체의 read 메쏘드를 호출하고 즐겁게 파싱을 합니다. 그 입력이 하드-코딩된 문자열에서 왔다는 사실을 알지 못하고 말입니다.

그래서 이제 함수 하나 minidom.parse의 사용법을 압니다. 로컬 파일이나 하드-코딩된 문자열 또는 웹에 저장된 XML 문서를 파싱할 수 있습니다. 웹 페이지에 대하여 urlopen을 사용하면 파일-류의 객체를 얻을 수 있습니다; 지역 파일에는 open을 사용하고; 문자열에는 StringIO를 사용합니다. 이제 한 걸음 더 나아가 이런 차이점들을 일반화 시켜 보겠습니다.

예제 10.6. openAnything

def openAnything(source):                  
    # urllib로 열려고 시도한다 (소스가 http나 ftp 또는 파일 URL이라면 말이다)
    import urllib                         
    try:                                  
        return urllib.urlopen(source)      
    except (IOError, OSError):            
        pass                              

    # 고유의 open 함수로 열려고 시도한다 (소스가 경로이름이라면 말이다)
    try:                                  
        return open(source)                
    except (IOError, OSError):            
        pass                              

    # 소스를 문자열이라고 간주한다
    import StringIO                       
    return StringIO.StringIO(str(source))  
openAnything 함수는 매개변수를 하나, source를 받아서, 파일-류의 객체를 돌려줍니다. source는 온갖 종류의 문자열입니다; ('http://slashdot.org/slashdot.rdf'처럼) URL일 수도 있고, ('binary.xml'처럼) 지역 파일에 대한 완전한 또는 부분적인 경로이름 일 수 있으며, 또는 파싱될 실제 XML 데이터를 담고 있는 문자열일 수도 있습니다.
먼저, sourceURL인지 살펴봅니다. 무차별적으로 이렇게 합니다: URL로 열려고 시도해 보고 에러가 일어나면 조용히 무시합니다. 이 방법은 실제로 다음과 같은 점에서 우아한데, 앞으로 urllib가 새로운 유형의 URL을 지원하더라도, 다시 코딩하지 않아도 되기 때문입니다. urllibsource를 열 수 있다면 함수에서 즉시 빠져 나오고(return) 다음 try 서술문을 실행되지 않습니다.
반면에, urllib가 신음하면서 source가 유효한 URL이 아니라고 말한다면 그것이 디스크에 저장된 파일에 대한 경로라고 간주하고 열어 봅니다. 역시, source가 유효한 파일이름인지 아닌지 검검하는 수고를 하지 않아도 됩니다 (유효한 파일이름에 대한 규칙은 플랫폼에 따라 제멋대로 바뀝니다. 그래서 엉터리로 파일이름을 얻을 가능성이 높습니다). 대신에, 무작정 파일을 엽니다. 그리고 에러가 있다면 조용하게 잡습니다.
이 시점에 이르면 (다른 것은 작동하지 않았기 때문에) source가 하드-코드된 데이터 문자열이라고 간주해야 합니다. 그래서 그로부터 StringIO를 사용하여 파일-류 객체를 만들어서 돌려줍니다. (사실, str 함수를 사용하기 때문에, source 는 문자열일 필요조차 없습니다; 어떤 객체든지 상관이 없으며, 그의 __str__ 특수 메쏘드에 정의된 문자열 표현을 사용하면 됩니다.)

이제 openAnything 함수를 minidom.parse와 더불어 사용하면 어떻게든 (URL이든 로컬 파일이든 또는 문자열에 하드-코딩된 XML 문서이든) XML 문서를 가리키는 source를 취해 그것을 파싱하는 함수를 만들 수 있습니다.

예제 10.7. kgp.py에서 openAnything 사용하기

class KantGenerator:
    def _load(self, source):
        sock = toolbox.openAnything(source)
        xmldoc = minidom.parse(sock).documentElement
        sock.close()
        return xmldoc

10.2. 표준 입력과 표준 출력 그리고 표준 에러

UNIX 세계의 사용자는 이미 표준 입력과 표준 출력 그리고 표준 에러의 개념에 익숙합니다. 다음 섹션은 그 나머지 세계의 사용자를 위한 섹션입니다.

표준 출력과 표준 에러는 (일반적으로 약어로 stdoutstderr로 사용하는데) 모든 UNIX 시스템에 내장된 파이프입니다. 무언가를 인쇄하면(print), stdout 파이프로 갑니다; 프로그램이 충돌하고 디버깅 정보를 인쇄할 때는 (파이썬에서 역추적 같이), stderr 파이프로 갑니다. 이 두 파이프 모두 보통 작업 중인 터미날 창에 접속됩니다. 그래서 프로그램이 인쇄하면 그 출력을 보게 됩니다. 프로그램이 충돌하면 디버깅 정보를 보게 됩니다. (창-기반의 파이썬 IDE를 가진 시스템에서 작업하고 있다면 stdoutstderr가 여러분의 “상호대화 창”에 기본 값이 됩니다.)

예제 10.8.  stdoutstderr 소개

>>> for i in range(3):
...     print 'Dive in'             
Dive in
Dive in
Dive in
>>> import sys
>>> for i in range(3):
...     sys.stdout.write('Dive in') 
Dive inDive inDive in
>>> for i in range(3):
...     sys.stderr.write('Dive in') 
Dive inDive inDive in
예제 6.9, “간단한 계수기”에서 보았듯이, 파이썬에 내장된 range 함수를 사용하면 지정된 횟수 만큼 무언가 반복하는 간단한 카운터 회돌이를 구축할 수 있습니다.
stdout은 파일-류의 객체입니다; 그의 write 함수를 호출하면 건넨 문자열을 무엇이든 인쇄합니다. 사실, 이것은 실제로 print 함수가 합니다; 이 함수는 줄넘김 문자를 인쇄할 문자열의 끝에 추가하고, sys.stdout.write를 호출합니다.
가장 단순한 경우, stdoutstderr는 출력을 같은 곳에 보냅니다: (통합환경에 있다면) 파이썬 IDE나 또는 (명령어 줄에서 파이썬을 실행중이라면) 터미날로 전송됩니다. stdout처럼, stderr는 여러분을 대신하여 줄넘김 문자를 추가하지 않습니다; 원한다면 직접 추가하세요.

stdoutstderr는 모두 파일-류의 객체이며, 섹션 10.1, “입력 소스 추상화”에서 연구한 객체와 비슷합니다. 그러나 둘 모두 쓰기 전용입니다. read 메쏘드가 없고, write 메쏘드만 있습니다. 여전히, 파일-류의 객체이며, 파일-객체 또는 파일-류의 객체를 거기에 할당하여 그 출력의 방향을 바꿀 수 있습니다.

예제 10.9. 출력 방향 전환

[you@localhost kgp]$ python stdout.py
Dive in
[you@localhost kgp]$ cat out.log
This message will be logged instead of displayed

(윈도우즈에서는 cat 대신에 type를 사용하여 파일의 내용을 화면에 표시할 수 있습니다.)

아직 그렇게 하지 않았다면 이 책에 사용된 이 예제와 기타 예제들을 내려 받을 수 있습니다.

#stdout.py
import sys

print 'Dive in'                                          
saveout = sys.stdout                                     
fsock = open('out.log', 'w')                             
sys.stdout = fsock                                       
print 'This message will be logged instead of displayed' 
sys.stdout = saveout                                     
fsock.close()                                            
이것은 IDE상호대화 창”에 인쇄됩니다; (또는 명령어 주에서 스크립트를 실행한다면 터미날에 인쇄한다).
항상 stdout에 저장하고 난 다음에 방향전환하세요. 그래야 나중에 정상으로 다시 설정할 수 있습니다.
읽기용으로 파일을 여세요. 파일이 존재하지 않으면 만들어집니다. 파일이 있으면 덮어 씌여집니다.
앞으로 출력은 모두 방금 연 새 파일로 방향전환됩니다.
이것은 로그 파일에만 “인쇄됩니다”; IDE 창이나 화면에서는 볼 수 없습니다.
가지고 놀기 전의 상태로 다시 stdout을 돌려 놓으세요.
로그 파일을 닫으세요.

stderr를 방향전환하는 것도 똑 같은 방식으로 작동합니다. sys.stdout 대신에 sys.stderr를 사용하면 됩니다.

예제 10.10. 에러 정보 방향전환하는 법

[you@localhost kgp]$ python stderr.py
[you@localhost kgp]$ cat error.log
Traceback (most recent line last):
  File "stderr.py", line 5, in ?
    raise Exception, 'this error will be logged'
Exception: this error will be logged

아직 그렇게 하지 않았다면 이 책에 사용된 다음 예제와 기타 예제들을 내려받을 수 있습니다.

#stderr.py
import sys

fsock = open('error.log', 'w')               
sys.stderr = fsock                           
raise Exception, 'this error will be logged'  
디버깅 정보를 저장하고 싶은 곳에서 로그 파일을 여세요.
표준 에러를 방향전환하세요. 새로-연 로그 파일 객체를 stderr에 할당하면 됩니다.
에러를 일으키세요. 화면 출력을 보면 아무것도 인쇄하지 않음을 주목하세요. 정상 출력은 모두 error.log에 씌여집니다.
또 주목하세요. 로그 파일을 명시적으로 닫지도 않으며, stderr에 다시 원래 값을 설정하지도 않습니다. (예외 때문에) 프로그램이 충돌하면 파이썬이 우리 대신 파일을 닫고 정리해 줍니다. 그리고 stderr를 복구하지 않더라도 마찬가지입니다, 왜냐하면 언급하였듯이 프로그램은 충돌하고 파이썬은 끝나기 때문입니다. stdout에 대하여 원래 값을 복구하는 일은 더 중요합니다. 앞으로 같은 스크립트에서 다른 일을 할 생각이라면 말입니다.

에러 메시지를 표준 에러 화면에 쓰는 일은 너무 흔하기 때문에, 지름길 구문이 있습니다. 무작정 방향전환하는 고생을 하는 대신에 이 구문을 사용할 수 있습니다.

예제 10.11. stderr에 인쇄하기

>>> print 'entering function'
entering function
>>> import sys
>>> print >> sys.stderr, 'entering function' 
entering function
print 서술문의 이 지름길 구문은 열린 파일이나 파일-류 객체에 쓰는데 사용할 수 있습니다. 이 경우 다음의 print 서술문에 영향을 끼치지 않고 단 하나의 print 서술문만 stderr에 방향전환시킬 수 있습니다.

반면에 표준 입력은 읽기-전용 파일 객체입니다. 그리고 앞의 프로그램으로부터 다른 프로그램 안으로 흘러들어가는 데이터를 나타냅니다. 이는 고전 Mac OS 사용자나 윈도우즈 사용자에게 별 의미가 없을 것입니다. MS-DOS 명령어 줄에 익숙하지 않는 한 말입니다. 작동하는 방식은 일련의 명령어를 한 줄에 구성해서, 프로그램의 출력이 사슬에서 다음 프로그램의 입력이 되도록 하는 것입니다. 첫 프로그램은 그냥 표준 출력에 출력합니다 (특별히 방향전환을 하지 않고, 그냥 보통의 print 서술문 등등을 사용합니다), 그리고 다음 프로그램이 표준 입력으로부터 읽습니다. 운영 체제는 책임지고 한 프로그램의 출력을 다음 프로그램의 입력에 연결시켜 줍니다.

예제 10.12. 명령어 연결하기

[you@localhost kgp]$ python kgp.py -g binary.xml         
01100111
[you@localhost kgp]$ cat binary.xml                      
<?xml version="1.0"?>
<!DOCTYPE grammar PUBLIC "-//diveintopython.org//DTD Kant Generator Pro v1.0//EN" "kgp.dtd">
<grammar>
<ref id="bit">
  <p>0</p>
  <p>1</p>
</ref>
<ref id="byte">
  <p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>\
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar>
[you@localhost kgp]$ cat binary.xml | python kgp.py -g -  
10110001
섹션 9.1, “다이빙해 들어가기”에서 보셨듯이, 이는 무작위로 0 또는 1의 비트 여덟개로 구성된 문자열을 인쇄합니다.
이는 단순히 binary.xml의 전체 내용을 인쇄합니다. (윈도우즈 사용자는 cat 대신에 type을 사용해야 합니다.)
이는 binary.xml의 내용을 인쇄하지만, “pipe”라고는 부르는 “|” 문자는 내용이 화면에 출력되지 않는다는 뜻입니다. 대신에, 다음 명령어의 표준 입력이 됩니다. 이경우, 파이썬 스크립트를 호출합니다.
(binary.xml 같이) 모듈을 지정하는 대신에, “-”를 지정하면 디스크에 있는 특정한 파일 대신에 표준 입력으로부터 문법을 적재하게 됩니다. (어떻게 이런 일이 일어나는지는 다음 예제에서 더 자세하게 다룹니다.) 그래서 그 효과는 맨 첫 구문과 같습니다. 거기에서는 문법 파일이름을 직접 지정했지만, 여기에서는 확장 가능성을 생각해 보세요. 단순히 cat binary.xml이라고 하는 대신, 동적으로 문법을 생성하는 스크립트를 실행할 수도 있습니다. 그래서 그 결과를 스크립트 안으로 파이프처리해 넣을 수 있습니다. 입력은 어떤 곳으로부터도 올 수 있습니다: 데이터베이스나 문법-생성 메타 스크립트 기타 등등. 요점은 이 기능을 병합해 넣기 위하여 kgp.py 스크립트를 전혀 고칠 필요가 없다는 것입니다. 문법 파일을 표준 입력으로 받을 수만 있으면 됩니다. 그러면 다른 로직은 모두 별도의 프로그램 안으로 분리해 넣을 수 있습니다.

그래서 스크립트는 문법 파일이 “-”일 때 어떻게 표준 입력을 읽어 들이는 방법을 “아는가”? 마법이 아닙니다; 그저 코드일 뿐입니다.

예제 10.13. kgp.py에서 표준 입력을 읽기

def openAnything(source):
    if source == "-":    
        import sys
        return sys.stdin

    # (소스가 http나 ftp 또는 파일 URL이라면) urllib로 열기 시도
    import urllib
    try:

[... 이하 생략 ...]
이는 toolbox.py에서 가져 온 openAnything 함수입니다. 이전에 섹션 10.1, “입력 소스 추상화”에서 본 적이 있습니다. 세줄의 코드를 함수의 첫 머리에 추가해서 소스가 “-”인지 알아봅니다; 그렇다면 sys.stdin을 돌려 줍니다. 실제로, sys.stdin이 반환됩니다! 기억하세요. stdin은 파일-류 객체로서 read 메쏘드가 있습니다. 그래서 (kgp.py에서 openAnything이라고 부르는) 나머지 코드는 하나도 바뀌지 않습니다.

10.3. 노드 검색 보관하기

kgp.pyXML 처리에 유용할 수도 유용하지 않을 수도 있는 여러 트릭을 채용합니다. 첫 트릭은 입력 문서의 일관된 구조를 이용하여 노드를 보관하는 것입니다.

문법 파일은 일련의 ref 원소를 정의합니다. 각 ref에는 하나 이상의 p 원소가 들어 있습니다. 여기에xref를 비롯하여 수 많은 것들이 담길 수 있습니다 . xref를 만날 때마다, 그에 상응하는 id 속성이 같은 ref 요소를 찾습니다. 그리고 ref 원소의 자손중 하나를 찾아 해석합니다. (다음 섹션에서 이런 무작위 선택이 어떻게 이루어지는지 살펴봅니다.)

다음과 같이 문법을 구축합니다: 가장 작은 조각에 대하여 ref 요소를 정의한 다음, xref를 사용해서 앞쪽 ref 요소들이 "포함된" ref 요소를 정의합니다. 등등. 다음으로 "가장 큰" 참조를 해석해서 각각의 xref를 따라갑니다. 그리고 마침내 진짜 텍스트가 출력됩니다. 출력된 텍스트는 xref를 채울 때마다 선택한 (무작위) 결정에 따라 다릅니다. 그래서 출력은 매번 다릅니다.

이는 아주 유연하지만, 한가지 단점이 있습니다: 수행성능이 떨어집니다. xref를 찾고 그에 상응하는 ref 원소를 찾을 필요가 있다면 문제가 있습니다. xrefid 속성이 있고, id 속성이 같은 ref 원소를 찾고 싶지만, 그렇게 할 방법이 없습니다. 느린 방법이 있다면 매번 ref 원소의 전체 리스트를 얻어서, 수작업으로 회돌이를 돌면서 각 id 속성을 살펴 보는 것입니다. 빠른 방법은 한 번 보고 나면 사전의 형태로 보관하는 것입니다.

예제 10.14. loadGrammar

    def loadGrammar(self, grammar):                         
        self.grammar = self._load(grammar)                  
        self.refs = {}                                       
        for ref in self.grammar.getElementsByTagName("ref"): 
            self.refs[ref.attributes["id"].value] = ref       
빈 사전 self.refs를 만들면서 시작합니다.
섹션 9.5, “원소 찾기”에서 보셨듯이, getElementsByTagName은 특정 이름의 요소들을 모두 담아 리스트로 돌려줍니다. 쉽게 모든 ref 원소의 리스트를 얻을 수 있습니다. 다음 그냥 그 리스트를 회돌이하면 됩니다.
섹션 9.6, “요소 속성에 접근하는 법”에서 보셨듯이, 표준 사전 구문을 사용하면 이름으로 원소의 속성에 따로따로 접근할 수 있습니다. 그래서 self.refs 사전의 키는 각 ref 원소의 id 속성의 값이 됩니다.
self.refs 사전의 값은 ref 원소 자체가 됩니다. 섹션 9.3, “XML 해석하기”에서 보셨듯이, 해석된 XML 문서에서 각 원소와 노드 그리고 주석과 텍스트는 객체입니다.

이렇게 간직하면 xref를 만날 때마다 id속성이 같은 ref 원소를 찾을 필요가 있을 경우, 그냥 self.refs에서 찾아보면 됩니다.

예제 10.15. ref 요소 보관소 사용하기

    def do_xref(self, node):
        id = node.attributes["id"].value
        self.parse(self.randomChildElement(self.refs[id]))

다음 섹션에서 randomChildElement 함수를 살펴보겠습니다.

10.4. 한 노드의 직계 자손 찾기

XML 문서를 해석할 때 또다른 유용한 테크닉은 특정 원소의 직계 자손 원소를 모두 찾는 것입니다. 예를 들면 문법 파일에서 ref 원소는 여러개의 p 원소가 있으며, 각 원소에는 다른 p 원소를 비롯하여 여러가지가 담길 수 있습니다. 다른 p 요소의 자손인 p 요소가 아니라, ref 요소의 자손인 p 요소만 찾고 싶습니다. .

여기에 그냥 getElementsByTagName를 사용하면 될 것 같지만, 안됩니다. getElementsByTagName는 재귀적으로 검색해서, 발견한 모든 요소들에 대하여 리스트 하나를 돌려줍니다. p 요소는 다른 p 요소를 담을 수 있기 때문에, getElementsByTagName을 사용할 수 없습니다. 뜻하지 않게 내포된 p 요소를 돌려주기 때문입니다. 오직 직계 자손만 찾고 싶다면 스스로 해야 합니다.

예제 10.16. 직계 자손 요소 찾기

    def randomChildElement(self, node):
        choices = [e for e in node.childNodes
                   if e.nodeType == e.ELEMENT_NODE]   
        chosen = random.choice(choices)             
        return chosen                              
예제 9.9, “자손 노드 얻는 법”에서 보셨듯이, childNodes 속성은 한 요소의 모든 자손 노드를 리스트로 돌려줍니다.
그렇지만, 예제 9.11, “자손 노드는 텍스트도 될 수 있다”에서 보셨듯이, childNodes가 돌려주는 리스트에는 텍스트 노드를 비롯하여 온갖 종류의 노드가 포함됩니다. 그 모든 것을 여기에서 원하는 것은 아닙니다. 오직 요소인 자손만 얻고 싶습니다.
각 노드는 nodeType 속성이 있습니다. 이 속성은 ELEMENT_NODE, TEXT_NODE, COMMENT_NODE, 또는 얼마든지 다른 값이 될 수 있습니다. 가능한 값의 완전한 목록은 xml.dom 꾸러미의 __init__.py 파일에 있습니다. (패키지에 관해서 더 자세한 정보는 섹션 9.2, “꾸러미(Packages)”를 참조하세요.) 그러나 요소인 노드에만 관심이 있습니다. 그래서 리스트를 여과해서 오직 nodeTypeELEMENT_NODE인 요소만 포함시킬 수 있습니다.
실제 요소를 담은 리스트가 있으면 그 중에 아무거나 하나 고르는 일은 어렵지 않습니다. 파이썬에는 random이라는 모듈이 따라 오는데 여러 유용한 기능을 포함하고 있습니다. random.choice 함수는 리스트를 취해 임의의 항목을 하나 돌려줍니다. 예를 들어, ref 요소에 여러 p 요소가 들어 있다면 choicesp 요소들을 담은 리스트가 될 것이고, chosen은 정확하게 그 중의 하나에 할당되어 무작위로 선택될 것입니다.

10.5. 노드 유형에 따라 따로따로 처리자 만드는 법

세 번째 유용한 XML 처리 팁은 노드 유형과 요소 이름에 근거하여 코드를 논리적 기능으로 분리하는 것입니다. 해석된 XML 문서는 다양한 유형의 노드로 구성됩니다. 각 노드는 파이썬 객체로 표현됩니다. 문서 자체의 루트 수준은 문서(Document) 객체로 나타냅니다. 문서(Document)에는 하나 이상의 요소(Element) 객체가 담깁니다. (실제 XML 태그에 대하여), 각 요소에는 다른 요소(Element) 객체나 (텍스트 부분을 위하여) 텍스트(Text) 객체 또는 (내장된 주석을 위하여) 주석(Comment) 객체가 담깁니다. 파이썬에서는 각 노드 유형에 대하여 로직을 따로따로 분리하기 위한 배포자를 작성하기가 쉽습니다.

예제 10.17. 해석된 XML 객체의 클래스 이름

>>> from xml.dom import minidom
>>> xmldoc = minidom.parse('kant.xml') 
>>> xmldoc
<xml.dom.minidom.Document instance at 0x01359DE8>
>>> xmldoc.__class__                   
<class xml.dom.minidom.Document at 0x01105D40>
>>> xmldoc.__class__.__name__          
'Document'
당분간 kant.xml이 현재 디렉토리에 있다고 간주하겠습니다.
섹션 9.2, “패키지(Packages)”에서 보셨듯이, XML 문서를 해석하면 돌려받는 객체는 Document 객체입니다. xml.dom 꾸러미의 minidom.py에 정의된 대로 말입니다. 섹션 5.4, “클래스 실체화하기”에서 보셨듯이, __class__는 모든 파이썬 객체에 내장된 속성입니다.
게다가, __name__은 모든 파이썬 클래스에 내장된 속성이며, 문자열입니다. 이 문자열은 비밀스럽지 않습니다; 클래스를 직접 정의할 때 타자한 클래스 이름과 같습니다. (섹션 5.3, “클래스 정의하기” 참조.)

좋습니다. 그래서 특정 XML 노드의 클래스 이름을 얻을 수 있습니다 (각 XML 노드는 파이썬 객체로 표현됩니다). 어떻게 이를 이용하여 각 노드 유형을 해석하는 로직을 떼어낼 수 있을까? 그 해답은 getattr입니다. 이 메쏘드는 섹션 4.4, “getattr로 객체 참조점을 얻기”에서 보셨습니다.

예제 10.18. parse, 총괄적인 XML 노드 분배자

    def parse(self, node):          
        parseMethod = getattr(self, "parse_%s" % node.__class__.__name__)  
        parseMethod(node) 
무엇보다, (node 인자에) 건넨 노드의 클래스 이름을 토대로 더 큰 문자열을 구성하고 있음에 주목하세요. 그래서 Document 노드를 건네면 'parse_Document'이라는 문자열을 구성합니다. 등등.
이제 그 문자열을 함수 이름으로 취급할 수 있으며, getattr을 사용하여 그 함수 자체에 대한 참조점을 얻을 수 있습니다
마지막으로, 그 함수를 호출하고 노드 그 자체를 인자로 건넬 수 있습니다. 다음 예제는 이런 함수들의 정의를 각각 보여줍니다.

예제 10.19. parse 분배자가 호출하는 함수들

    def parse_Document(self, node): 
        self.parse(node.documentElement)

    def parse_Text(self, node):    
        text = node.data
        if self.capitalizeNextWord:
            self.pieces.append(text[0].upper())
            self.pieces.append(text[1:])
            self.capitalizeNextWord = 0
        else:
            self.pieces.append(text)

    def parse_Comment(self, node): 
        pass

    def parse_Element(self, node): 
        handlerMethod = getattr(self, "do_%s" % node.tagName)
        handlerMethod(node)
parse_Document는 오직 한 번만 호출되는데, 그 이유는 XML 문서에 오직 하나의 Document 노드만 있고, 해석된 XML 표현에 오직 하나의 Document 객체만 있기 때문입니다. 그냥 문법 파일의 루트 요소를 해석하여 돌려줍니다.
parse_Text는 텍스트를 대표하는 노드에 대하여 호출됩니다. 함수 그 자체로 문장의 첫 단어를 대문자로 자동으로 처리하기 위하여 어떤 특별한 처리를 합니다. 그러나 그렇지 않으면 표현된 텍스트를 그냥 리스트에 추가합니다.
parse_Comment는 그냥 pass에 불과합니다. 문법 파일에 내장된 주석에 관하여 신경쓰지 않기 때문입니다. 그렇지만 주의하세요. 여전히 함수를 정의하고 명시적으로 아무것도 하지 못하도록 할 필요가 있습니다. 함수가 존재하지 않으면 일반 parse 함수는 주석에 부딪치자 마자 실패합니다. 왜냐하면 존재하지 않는 parse_Comment 함수를 찾으려고 시도하기 때문입니다. 각 노드 유형마다 따로, 심지어 사용하지 않는 유형조차도 함수를 정의하면 일반 parse 함수가 그냥 조용히 멍청하게 있을 수 있습니다..
parse_Element 메쏘드는 실제로 그 요소의 태그에 기반한 배분자입니다. 기본 아이디어는 같습니다: 요소를 다른 요소 (태그 이름)과 구분짓는 것을 받아서 각각에 대하여 별도의 함수에 배분하는 것입니다. (<xref> 태그에 대하여) 'do_xref' 같은 문자열을 구성하고, 그 이름으로 된 함수를 찾아 호출합니다. 등등 문법 파일을 해석하는 동안에 발견될 기타 다른 태그 이름마다 모두 호출합니다 (<p> 태그, <choice> 태그).

이 예제에서, 분배(dispatch) 함수인 parseparse_Element는 단순히 같은 클래스 안에서 다른 메쏘드들을 찾습니다. 처리가 아주 복잡하다면 (또는 태그 이름이 많다면), 코드를 별도의 모듈로 쪼개서, 동적으로 각 모듈을 반입하면 됩니다. 그리고 필요한 함수를 호출하면 됩니다. 동적인 반입은 제 16 장, 기능형 프로그래밍에서 다르겠습니다.

10.6. 명령어-줄 인자 처리하기

파이썬은 프로그램을 만드는 것을 완벽하게 지원합니다. 명령어 줄에서 실행되고, 명령어-줄 인자 그리고 다양한 옵션을 지정하는 긴-스타일 또는 짧은-스타일의 플래그를 완벽하게 갖추고 있습니다. 이 중 어느 것도 XML-종속적이지는 않지만, 이 스크립트는 명령어-줄 처리를 잘 활용합니다. 그래서 그에 관하여 언급할 좋은 시간인 듯 보입니다.

명령어-줄 인자가 파이썬 프로그램에 어떻게 노출되는지 이해하지 않고서는 명령어-줄 처리를 언급하는 것은 어렵습니다. 그래서 간단한 프로그램을 만들어 알아 보겠습니다.

예제 10.20.  sys.argv 소개

아직 그렇게 하지 못했다면 이 책에 사용된 예제를 내려 받을 수 있습니다.

#argecho.py
import sys

for arg in sys.argv: 
    print arg
프로그램에 건넨 명령어-줄 인자는 sys.argv에 존재합니다. 이는 그냥 리스트입니다. 다음에 줄마다 따로따로 인자를 인쇄하여 보았습니다.

예제 10.21.  sys.argv의 내용

[you@localhost py]$ python argecho.py             
argecho.py
[you@localhost py]$ python argecho.py abc def     
argecho.py
abc
def
[you@localhost py]$ python argecho.py --help      
argecho.py
--help
[you@localhost py]$ python argecho.py -m kant.xml 
argecho.py
-m
kant.xml
제일 먼저 sys.argv에 관하여 알아야 할 일은 호출하고 있는 스크립트의 이름이 그 안에 담겨 있다는 사실입니다. 실제로 나중에 제 16장, 기능적 프로그래밍에서 이 지식을 활용합니다. 지금 당장은 걱정하지 마세요.
명령어-줄 인자는 공백문자로 가르며, 따로따로 sys.argv 리스트에 원소로 나타납니다.
--help 같은 명령어-줄 플래그도 sys.argv 리스트에 원소로 나타납니다.
더욱 더 재미있게도, 어떤 명령어-줄 플래그는 그 자체로 인자를 취합니다. 예를 들면 여기에 인자(kant.xml)를 취하는 플래그(-m)가 있습니다. 플래그나 그의 인자 모두 그냥 sys.argv 리스트에 연속적으로 나타나는 원소입니다. 서로 엮으려는 시도는 전혀 이루어지지 않습니다; 오직 리스트만 받을 뿐입니다.

그래서 보시다시피, 명령어줄 인자에 건넨 정보는 확실하게 모두 확보합니다. 그러나, 실제로 사용하기는 그렇게 쉬어 보이지 않습니다. 인자를 달랑 하나만 취하고 플래그는 전혀 없는 간단한 프로그램이라면 그냥 sys.argv[1]를 이용하여 그 인자에 접근하면 됩니다. 이렇게 하더라도 전혀 부끄러울 것이 없습니다; 본인은 항상 그렇게 처리합니다. 좀 더 복잡한 프로그램이라면 getopt 모듈이 필요합니다.

예제 10.22.  getopt 소개

def main(argv):                         
    grammar = "kant.xml"                 
    try:                                
        opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="]) 
    except getopt.GetoptError:           
        usage()                          
        sys.exit(2)                     

...

if __name__ == "__main__":
    main(sys.argv[1:])
무엇보다도, 예제의 아래쪽을 보시면 sys.argv[1:]main 함수를 호출하고 있는 것에 주목하세요. 기억하세요. sys.argv[0]는 실행중인 스크립트의 이름입니다; 이는 명령어-줄 처리에 대하여 신경쓸 필요가 없습니다. 그래서 잘라내어 버리고 나머지 리스트를 건넵니다.
이곳에서 모든 흥미로운 처리가 일어납니다. getopt 모듈의 getopt 함수는 매개변수를 세 개 받습니다: (sys.argv[1:]에서 얻은) 인자 리스트와 이 프로그램이 받는 한-문자짜리 명령-줄 플래그를 모두 담고 있는 문자열 그리고 한-문자짜리 버전과 동등한 좀 더 긴 명령-줄 플래그 리스트가 그것입니다. 처음 보면 이것이 상당히 혼란스럽습니다. 아래에서 좀 더 자세하게 다루겠습니다.
이 명령-줄 플래그를 해석하다가 일이 잘못되면 getopt는 예외를 일으키고 그 예외를 잡습니다. getopt에게 여러분이 아는 플래그를 모두 말해 주었습니다. 그래서 이는 아마도 여러분이 알지 못하는 어떤 명령-줄을 최종 사용자가 건넸다는 뜻입니다.
UNIX 세계에서는 표준적인 관행으로서, 스크립트에 이해하지 못하는 플래그가 건네지면 적절한 사용법을 인쇄하고 우아하게 종료합니다. 여기에서 usage 함수를 보여주지 않았음에 주목하세요. 어디엔가 그것을 코드해서 적절한 요약을 인쇄시켜야 하겠습니다; 자동이 아닙니다.

그래서 getopt 함수에 건넨 그런 매개변수들은 무엇인가? 음, 첫 매개변수는 그냥 명령-줄 플래그와 인자를 담은 날 리스트입니다 (첫 인자인 스크립트 이름은 포함되지 않는데, 이는 이미 main 함수를 호출하기 전에 잘라내어 버렸습니다). 두 번째 매개변수는 스크립트가 받은 짧은 명령-줄 플래그의 리스트입니다.

"hg:d"

-h
print usage summary
-g ...
use specified grammar file or URL
-d
show debugging information while parsing

첫 플래그와 세번째 플래그는 그냥 독립 플래그입니다; 지정해도 되고 안 해도 됩니다. 플래그는 일을 하거나 (도움말 인쇄) 상태를 바꿉니다 (디버깅 활성화). 그렇지만, 두번째 플래그(-g)는 반드시 인자 다음에 와야 합니다. 인자는 읽어 들일 문법 파일의 이름입니다. 실제로는 파일이름이나 웹 주소가 될 수 있으며, 아직 어느 것인지 알지 못합니다 (나중에 알아 보겠습니다). 그러나, 무언가 되어야 한다는 것은 압니다. 그래서 getopt에게 이 사실을 알려줍니다. getopt 함수의 두 번째 매개변수에 있는 g 뒤에 쌍점을 두어서 말입니다.

더욱 복잡하게도, 스크립트는 (-h같은) 짧은 플래그나 (--help 같은) 긴 플래그 어느 쪽도 받습니다. 그리고 두 가지가 같은 일을 하기를 원합니다. 이는 getopt에 건넨 세 번째 매개변수가 하는 일입니다. 두 번째 매개변수에 지정한 짧은 플래그에 상응하는 긴 플래그 리스트를 지정합니다.

["help", "grammar="]

--help
print usage summary
--grammar ...
use specified grammar file or URL

여기에서 주목할 세가지:

  1. 긴 플래그는 모두 명령 줄에서 앞에 옆줄 문자가 두개 있습니다. 그러나 getopt를 호출할 때는 포함시키지 않습니다. 포함시키지 않아도 이해합니다.
  2. --grammar 플래그는 언제나 다음에 예를 들어 -g 플래그 같은 추가 인자가 따라 와야 합니다. 이는 등호 사인 "grammar="으로 표시됩니다.
  3. 여기에서 긴-스타일의 플래그 리스트가 짧은-스타일의 플래그 리스트보다 짧습니다. 왜냐하면 -d 플래그에 그에 상응하는 긴 버전이 포함되지 않기 때문입니다. 이것은 좋은 일입니다; -d는 디버깅만 활성화 시킵니다. 그러나 짧은 플래그와 긴 플래그의 순서는 같을 필요가 있습니다. 그래서 먼저 긴 플래그에 상응하는 짧은 플래그를 모두 지정한 다음, 나머지 짧은 플래그를 모두 지정할 필요가 있습니다.

아직 잘 모르시겠습니까? 실제 코드를 살펴보고 문맥에서 의미가 있는지 알아 보겠습니다.

예제 10.23. kgp.py에서 명령-줄 인자 처리하기

def main(argv):                          
    grammar = "kant.xml"                
    try:                                
        opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="])
    except getopt.GetoptError:          
        usage()                         
        sys.exit(2)                     
    for opt, arg in opts:                
        if opt in ("-h", "--help"):      
            usage()                     
            sys.exit()                  
        elif opt == '-d':                
            global _debug               
            _debug = 1                  
        elif opt in ("-g", "--grammar"): 
            grammar = arg               

    source = "".join(args)               

    k = KantGenerator(grammar, source)
    print k.output()
grammar 변수는 사용중인 문법 파일을 추적 유지합니다. 명령 줄에 지정되어 있지 않을 경우 (-g 플래그 또는 --grammar 플래그를 사용하여) 여기에서 초기화합니다.
getopt로부터 돌려받은 opts 변수에는 터플의 리스트가 담깁니다: 플래그(flag)와 변수(argument). 플래그가 인자를 받지 않으면 arg는 그냥 None이 됩니다. 이렇게 하면 플래그를 회돌이하기가 더 쉽습니다.
getopt는 명령-줄 플래그가 받아 들여도 좋은지 평가합니다. 그러나 긴 플래그와 짧은 플래그 사이를 전혀 변환하지 않습니다. -h 플래그를 지정하면 opt에는 "-h"가 담깁니다; --help 플래그를 지정하면 opt에는 "--help"가 담깁니다. 그래서 둘 모두 점검할 필요가 있습니다.
기억하세요. -d 플래그는 상응하는 긴 플래그가 없습니다. 그래서 짧은 형태만 점검하면 됩니다. 이 플래그를 발견하면 나중에 디버깅 정보를 출력하기 위하여 참조하겠다고 전역 변수에 설정합니다. (스크립트를 개발하면서 이 방법을 사용했습니다. 설마, 예제들이 시작부터 잘 작동하리라고 생각하신건 아니겠지요?)
-g 플래그나 --grammar 플래그로 문법 파일을 발견하면 다음에 따라 오는 (arg에 저장된) 인자를 grammar 변수에 저장하여, main 함수의 상단에서 초기화한 기본 값을 덮어씁니다.
성공입니다. 모든 명령-줄을 회돌이하면서 처리했습니다. 즉 이제 남은 것은 명령-줄 인자라는 뜻입니다. 명령-줄 인자는 getopt 함수로부터 args 변수에 다시 돌아옵니다. 이 경우, 그것들을 해석기를 위한 소스 재료로 취급하고 있습니다. 아무 명령-줄 인자도 지정되지 않으면 args는 빈 리스트가 됩니다. 그리고 source는 빈 문자열이 될 것입니다.

10.7. 모두 하나로 조립하기

기본 바탕을 많이 다루어 보았습니다. 한 발 물러서서 어떻게 조각들이 딱 맞게 조립되는지 보겠습니다.

먼저, 다음은 getopt 모듈을 사용하여 명령줄에서 그의 인자를 받는 스크립트입니다.

def main(argv):                         
...
    try:                                
        opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="])
    except getopt.GetoptError:          
...
    for opt, arg in opts:               
...

KantGenerator 클래스의 실체를 새로 만들어서, 거기에 문법 파일과 소스를 건넵니다. 이는 명령 줄에서 지정될 수 도 있고 안 될 수도 있습니다.

    k = KantGenerator(grammar, source)

KantGenerator 실체는 자동으로 문법파일을 적재합니다. 문법 파일은 XML 파일입니다. 맞춤 openAnything 함수를 사용하여 그 파일을 엽니다 (이 파일은 지역 파일 또는 원격 웹 서버에 저장되어 있을 수 있습니다). 다음 내장 minidom 해석 함수를 사용하여 XML 파일을 파이썬 객체 트리로 해석합니다.

    def _load(self, source):
        sock = toolbox.openAnything(source)
        xmldoc = minidom.parse(sock).documentElement
        sock.close()

아, 그러는 동안, XML 문서의 구조에 관한 지식을 활용하여 약간의 참조 캐시를 설정합니다. 이는 그냥 XML 문서의 원소들일 뿐입니다.

    def loadGrammar(self, grammar):                         
        for ref in self.grammar.getElementsByTagName("ref"):
            self.refs[ref.attributes["id"].value] = ref     

명령 줄에 소스 재료를 지정했다면 그것을 사용합니다; 그렇지 않으면 문법 파일을 헤집고 들어가 (다른 어떤 것에도 참조되지 않는) "최상위-수준의" 참조점을 찾아서 시작 지점으로 사용합니다.

    def getDefaultSource(self):
        xrefs = {}
        for xref in self.grammar.getElementsByTagName("xref"):
            xrefs[xref.attributes["id"].value] = 1
        xrefs = xrefs.keys()
        standaloneXrefs = [e for e in self.refs.keys() if e not in xrefs]
        return '<xref id="%s"/>' % random.choice(standaloneXrefs)

이제 소스 재료를 찾습니다. 소스 재료도 XML 파일이며, 한 번에 한 노드씩 해석합니다. 코드를 따로 관리하고 좀 유지보성이 높게 하기 위하여, 각 노드 유형에 대하여 별도의 처리자를 사용합니다.

    def parse_Element(self, node): 
        handlerMethod = getattr(self, "do_%s" % node.tagName)
        handlerMethod(node)

문법 파일을 뛰어 다니며, 각 p 요소의 모든 자손을 해석합니다.

    def do_p(self, node):
...
        if doit:
            for child in node.childNodes: self.parse(child)

choice 요소를 자손중에서 무작위로 교체하고,

    def do_choice(self, node):
        self.parse(self.randomChildElement(node))

xref 요소를 그에 상응하는 ref 요소의 자손중에서 무작위로 교체합니다.

    def do_xref(self, node):
        id = node.attributes["id"].value
        self.parse(self.randomChildElement(self.refs[id]))

드디어, 해석을 거쳐서 평범한 텍스트에 도달하였습니다.

    def parse_Text(self, node):    
        text = node.data
...
            self.pieces.append(text)

그것을 출력합니다.

def main(argv):                         
...
    k = KantGenerator(grammar, source)
    print k.output()

10.8. 요약

파이썬에는 XML 문서를 해석하고 조작하기 위한 강력한 라이브러리가 따라옵니다. minidomXML 파일을 받아 파이썬 객체로 해석해 주므로, 임의의 원소에 마음대로 접근할 수 있습니다. 게다가, 이 장에서는 파이썬을 사용하여 명령-줄 플래그와 명령-줄 인자 그리고 에러 처리로 완성된 "진짜" 독립적인 명령어-줄 스크립트를 만드는 법을 보여줍니다. 그리고 심지어 앞의 프로그램의 결과를 파이프로 입력을 취하는 능력을 보여주었습니다.

다음 장으로 나아가기 전에, 먼저 이 모든 것들을 제대로 익혀야 하겠습니다:

☜ 제 09 장 XML 처리 """ Dive Into Python """
다이빙 파이썬
제 11 장 HTTP 웹 서비스 ☞