home *** CD-ROM | disk | FTP | other *** search
/ OS/2 Shareware BBS: 10 Tools / 10-Tools.zip / pyth_os2.zip / python-1.0.2 / Lib / urllib.py < prev    next >
Text File  |  1994-04-18  |  12KB  |  457 lines

  1. # Open an arbitrary URL
  2. #
  3. # See the following document for a tentative description of URLs:
  4. #     Uniform Resource Locators              Tim Berners-Lee
  5. #     INTERNET DRAFT                                    CERN
  6. #     IETF URL Working Group                    14 July 1993
  7. #     draft-ietf-uri-url-01.txt
  8. #
  9. # The object returned by URLopener().open(file) will differ per
  10. # protocol.  All you know is that is has methods read(), readline(),
  11. # readlines(), fileno(), close() and info().  The read*(), fileno()
  12. # and close() methods work like those of open files. 
  13. # The info() method returns an rfc822.Message object which can be
  14. # used to query various info about the object, if available.
  15. # (rfc822.Message objects are queried with the getheader() method.)
  16.  
  17. import socket
  18. import regex
  19.  
  20.  
  21. # This really consists of two pieces:
  22. # (1) a class which handles opening of all sorts of URLs
  23. #     (plus assorted utilities etc.)
  24. # (2) a set of functions for parsing URLs
  25. # XXX Should these be separated out into different modules?
  26.  
  27.  
  28. # Shortcut for basic usage
  29. _urlopener = None
  30. def urlopen(url):
  31.     global _urlopener
  32.     if not _urlopener:
  33.         _urlopener = URLopener()
  34.     return _urlopener.open(url)
  35. def urlretrieve(url):
  36.     global _urlopener
  37.     if not _urlopener:
  38.         _urlopener = URLopener()
  39.     return _urlopener.retrieve(url)
  40. def urlcleanup():
  41.     if _urlopener:
  42.         _urlopener.cleanup()
  43.  
  44.  
  45. # Class to open URLs.
  46. # This is a class rather than just a subroutine because we may need
  47. # more than one set of global protocol-specific options.
  48. ftpcache = {}
  49. class URLopener:
  50.  
  51.     # Constructor
  52.     def __init__(self):
  53.         self.addheaders = []
  54.         self.tempcache = {}
  55.         self.ftpcache = ftpcache
  56.         # Undocumented feature: you can use a different
  57.         # ftp cache by assigning to the .ftpcache member;
  58.         # in case you want logically independent URL openers
  59.  
  60.     def __del__(self):
  61.         self.close()
  62.  
  63.     def close(self):
  64.         self.cleanup()
  65.  
  66.     def cleanup(self):
  67.         import os
  68.         for url in self.tempcache.keys():
  69.             try:
  70.                 os.unlink(self.tempcache[url][0])
  71.             except os.error:
  72.                 pass
  73.             del self.tempcache[url]
  74.  
  75.     # Add a header to be used by the HTTP interface only
  76.     # e.g. u.addheader('Accept', 'sound/basic')
  77.     def addheader(self, *args):
  78.         self.addheaders.append(args)
  79.  
  80.     # External interface
  81.     # Use URLopener().open(file) instead of open(file, 'r')
  82.     def open(self, url):
  83.         type, url = splittype(unwrap(url))
  84.          if not type: type = 'file'
  85.         name = 'open_' + type
  86.         if '-' in name:
  87.             import regsub
  88.             name = regsub.gsub('-', '_', name)
  89.         if not hasattr(self, name):
  90.             raise IOError, ('url error', 'unknown url type', type)
  91.         try:
  92.             return getattr(self, name)(url)
  93.         except socket.error, msg:
  94.             raise IOError, ('socket error', msg)
  95.  
  96.     # External interface
  97.     # retrieve(url) returns (filename, None) for a local object
  98.     # or (tempfilename, headers) for a remote object
  99.     def retrieve(self, url):
  100.         if self.tempcache.has_key(url):
  101.             return self.tempcache[url]
  102.         url1 = unwrap(url)
  103.         if self.tempcache.has_key(url1):
  104.             self.tempcache[url] = self.tempcache[url1]
  105.             return self.tempcache[url1]
  106.         type, url1 = splittype(url1)
  107.         if not type or type == 'file':
  108.             try:
  109.                 fp = self.open_local_file(url1)
  110.                 del fp
  111.                 return splithost(url1)[1], None
  112.             except IOError, msg:
  113.                 pass
  114.         fp = self.open(url)
  115.         headers = fp.info()
  116.         import tempfile
  117.         tfn = tempfile.mktemp()
  118.         self.tempcache[url] = result = tfn, headers
  119.         tfp = open(tfn, 'w')
  120.         bs = 1024*8
  121.         block = fp.read(bs)
  122.         while block:
  123.             tfp.write(block)
  124.             block = fp.read(bs)
  125.         del fp
  126.         del tfp
  127.         return result
  128.  
  129.     # Each method named open_<type> knows how to open that type of URL
  130.  
  131.     # Use HTTP protocol
  132.     def open_http(self, url):
  133.         import httplib
  134.         host, selector = splithost(url)
  135.         if not host: raise IOError, ('http error', 'no host given')
  136.         h = httplib.HTTP(host)
  137.         h.putrequest('GET', selector)
  138.         for args in self.addheaders: apply(h.putheader, args)
  139.         errcode, errmsg, headers = h.getreply()
  140.         if errcode == 200: return addinfo(h.getfile(), headers)
  141.         else: raise IOError, ('http error', errcode, errmsg, headers)
  142.  
  143.     # Use Gopher protocol
  144.     def open_gopher(self, url):
  145.         import gopherlib
  146.         host, selector = splithost(url)
  147.         if not host: raise IOError, ('gopher error', 'no host given')
  148.         type, selector = splitgophertype(selector)
  149.         selector, query = splitquery(selector)
  150.         if query: fp = gopherlib.send_query(selector, query, host)
  151.         else: fp = gopherlib.send_selector(selector, host)
  152.         return addinfo(fp, noheaders())
  153.  
  154.     # Use local file or FTP depending on form of URL
  155.     def open_file(self, url):
  156.         try:
  157.             return self.open_local_file(url)
  158.         except IOError:
  159.             return self.open_ftp(url)
  160.  
  161.     # Use local file
  162.     def open_local_file(self, url):
  163.         host, file = splithost(url)
  164.         if not host: return addinfo(open(file, 'r'), noheaders())
  165.         host, port = splitport(host)
  166.         if not port and socket.gethostbyname(host) in (
  167.               localhost(), thishost()):
  168.             return addinfo(open(file, 'r'), noheaders())
  169.         raise IOError, ('local file error', 'not on local host')
  170.  
  171.     # Use FTP protocol
  172.     def open_ftp(self, url):
  173.         host, file = splithost(url)
  174.         if not host: raise IOError, ('ftp error', 'no host given')
  175.         host, port = splitport(host)
  176.         host = socket.gethostbyname(host)
  177.         if not port:
  178.             import ftplib
  179.             port = ftplib.FTP_PORT
  180.         key = (host, port)
  181.         try:
  182.             if not self.ftpcache.has_key(key):
  183.                 self.ftpcache[key] = ftpwrapper(host, port)
  184.             return addinfo(self.ftpcache[key].retrfile(file),
  185.                   noheaders())
  186.         except ftperrors(), msg:
  187.             raise IOError, ('ftp error', msg)
  188.  
  189.  
  190. # Utility functions
  191.  
  192. # Return the IP address of the magic hostname 'localhost'
  193. _localhost = None
  194. def localhost():
  195.     global _localhost
  196.     if not _localhost:
  197.         _localhost = socket.gethostbyname('localhost')
  198.     return _localhost
  199.  
  200. # Return the IP address of the current host
  201. _thishost = None
  202. def thishost():
  203.     global _thishost
  204.     if not _thishost:
  205.         _thishost = socket.gethostbyname(socket.gethostname())
  206.     return _thishost
  207.  
  208. # Return the set of errors raised by the FTP class
  209. _ftperrors = None
  210. def ftperrors():
  211.     global _ftperrors
  212.     if not _ftperrors:
  213.         import ftplib
  214.         _ftperrors = (ftplib.error_reply,
  215.                   ftplib.error_temp,
  216.                   ftplib.error_perm,
  217.                   ftplib.error_proto)
  218.     return _ftperrors
  219.  
  220. # Return an empty rfc822.Message object
  221. _noheaders = None
  222. def noheaders():
  223.     global _noheaders
  224.     if not _noheaders:
  225.         import rfc822
  226.         _noheaders = rfc822.Message(open('/dev/null', 'r'))
  227.         _noheaders.fp.close()    # Recycle file descriptor
  228.     return _noheaders
  229.  
  230.  
  231. # Utility classes
  232.  
  233. # Class used by open_ftp() for cache of open FTP connections
  234. class ftpwrapper:
  235.     def __init__(self, host, port):
  236.         self.host = host
  237.         self.port = port
  238.         self.init()
  239.     def init(self):
  240.         import ftplib
  241.         self.ftp = ftplib.FTP()
  242.         self.ftp.connect(self.host, self.port)
  243.         self.ftp.login()
  244.     def retrfile(self, file):
  245.         import ftplib
  246.         try:
  247.             self.ftp.voidcmd('TYPE I')
  248.         except ftplib.all_errors:
  249.             self.init()
  250.             self.ftp.voidcmd('TYPE I')
  251.         conn = None
  252.         if file:
  253.             try:
  254.                 cmd = 'RETR ' + file
  255.                 conn = self.ftp.transfercmd(cmd)
  256.             except ftplib.error_perm, reason:
  257.                 if reason[:3] != '550':
  258.                     raise IOError, ('ftp error', reason)
  259.         if not conn:
  260.             # Try a directory listing
  261.             if file: cmd = 'LIST ' + file
  262.             else: cmd = 'LIST'
  263.             conn = self.ftp.transfercmd(cmd)
  264.         return addclosehook(conn.makefile('r'), self.ftp.voidresp)
  265.  
  266. # Base class for addinfo and addclosehook
  267. class addbase:
  268.     def __init__(self, fp):
  269.         self.fp = fp
  270.         self.read = self.fp.read
  271.         self.readline = self.fp.readline
  272.         self.readlines = self.fp.readlines
  273.         self.fileno = self.fp.fileno
  274.     def __repr__(self):
  275.         return '<%s at %s whose fp = %s>' % (
  276.               self.__class__.__name__, `id(self)`, `self.fp`)
  277.     def __del__(self):
  278.         self.close()
  279.     def close(self):
  280.         self.read = None
  281.         self.readline = None
  282.         self.readlines = None
  283.         self.fileno = None
  284.         self.fp = None
  285.  
  286. # Class to add a close hook to an open file
  287. class addclosehook(addbase):
  288.     def __init__(self, fp, closehook, *hookargs):
  289.         addbase.__init__(self, fp)
  290.         self.closehook = closehook
  291.         self.hookargs = hookargs
  292.     def close(self):
  293.         if self.closehook:
  294.             apply(self.closehook, self.hookargs)
  295.             self.closehook = None
  296.             self.hookargs = None
  297.         addbase.close(self)
  298.  
  299. # class to add an info() method to an open file
  300. class addinfo(addbase):
  301.     def __init__(self, fp, headers):
  302.         addbase.__init__(self, fp)
  303.         self.headers = headers
  304.     def info(self):
  305.         return self.headers
  306.  
  307.  
  308. # Utility to combine a URL with a base URL to form a new URL
  309.  
  310. def basejoin(base, url):
  311.     type, path = splittype(url)
  312.     if type: return url
  313.     host, path = splithost(path)
  314.     basetype, basepath = splittype(base)
  315.     basehost, basepath = splithost(basepath)
  316.     basepath, basetag = splittag(basepath)
  317.     basepath, basequery = splitquery(basepath)
  318.     type = basetype or 'file'
  319.     if path[:1] != '/':
  320.         import string
  321.         i = string.rfind(basepath, '/')
  322.         if i < 0: basepath = '/'
  323.         else: basepath = basepath[:i+1]
  324.         path = basepath + path
  325.     if not host: host = basehost
  326.     if host: return type + '://' + host + path
  327.     else: return type + ':' + path
  328.  
  329.  
  330. # Utilities to parse URLs:
  331. # unwrap('<URL:type//host/path>') --> 'type//host/path'
  332. # splittype('type:opaquestring') --> 'type', 'opaquestring'
  333. # splithost('//host[:port]/path') --> 'host[:port]', '/path'
  334. # splitport('host:port') --> 'host', 'port'
  335. # splitquery('/path?query') --> '/path', 'query'
  336. # splittag('/path#tag') --> '/path', 'tag'
  337. # splitgophertype('/Xselector') --> 'X', 'selector'
  338. # unquote('abc%20def') -> 'abc def'
  339. # quote('abc def') -> 'abc%20def')
  340.  
  341. def unwrap(url):
  342.     import string
  343.     url = string.strip(url)
  344.     if url[:1] == '<' and url[-1:] == '>':
  345.         url = string.strip(url[1:-1])
  346.     if url[:4] == 'URL:': url = string.strip(url[4:])
  347.     return url
  348.  
  349. _typeprog = regex.compile('^\([^/:]+\):\(.*\)$')
  350. def splittype(url):
  351.     if _typeprog.match(url) >= 0: return _typeprog.group(1, 2)
  352.     return None, url
  353.  
  354. _hostprog = regex.compile('^//\([^/]+\)\(.*\)$')
  355. def splithost(url):
  356.     if _hostprog.match(url) >= 0: return _hostprog.group(1, 2)
  357.     return None, url
  358.  
  359. _portprog = regex.compile('^\(.*\):\([0-9]+\)$')
  360. def splitport(host):
  361.     if _portprog.match(host) >= 0: return _portprog.group(1, 2)
  362.     return host, None
  363.  
  364. _queryprog = regex.compile('^\(.*\)\?\([^?]*\)$')
  365. def splitquery(url):
  366.     if _queryprog.match(url) >= 0: return _queryprog.group(1, 2)
  367.     return url, None
  368.  
  369. _tagprog = regex.compile('^\(.*\)#\([^#]*\)$')
  370. def splittag(url):
  371.     if _tagprog.match(url) >= 0: return _tagprog.group(1, 2)
  372.     return url, None
  373.  
  374. def splitgophertype(selector):
  375.     if selector[:1] == '/' and selector[1:2]:
  376.         return selector[1], selector[2:]
  377.     return None, selector
  378.  
  379. _quoteprog = regex.compile('%[0-9a-fA-F][0-9a-fA-F]')
  380. def unquote(s):
  381.     import string
  382.     i = 0
  383.     n = len(s)
  384.     res = ''
  385.     while 0 <= i < n:
  386.         j = _quoteprog.search(s, i)
  387.         if j < 0:
  388.             res = res + s[i:]
  389.             break
  390.         res = res + (s[i:j] + chr(eval('0x' + s[j+1:j+3])))
  391.         i = j+3
  392.     return res
  393.  
  394. _acceptable = \
  395.       'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@'
  396. def quote(s):
  397.     res = ''
  398.     for c in s:
  399.         if c in _acceptable: res = res + c
  400.         else: res = res + '%%%02x' % ord(c)
  401.     return res
  402.  
  403. # Test and time quote() and unquote()
  404. def test1():
  405.     import time
  406.     s = ''
  407.     for i in range(256): s = s + chr(i)
  408.     s = s*4
  409.     t0 = time.time()
  410.     qs = quote(s)
  411.     uqs = unquote(qs)
  412.     t1 = time.time()
  413.     if uqs != s:
  414.         print 'Wrong!'
  415.     print `s`
  416.     print `qs`
  417.     print `uqs`
  418.     print round(t1 - t0, 3), 'sec'
  419.  
  420.  
  421. # Test program
  422. def test():
  423.     import sys
  424.     import regsub
  425.     args = sys.argv[1:]
  426.     if not args:
  427.         args = [
  428.             '/etc/passwd',
  429.             'file:/etc/passwd',
  430.             'file://localhost/etc/passwd',
  431.             'ftp://ftp.cwi.nl/etc/passwd',
  432.             'gopher://gopher.cwi.nl/11/',
  433.             'http://www.cwi.nl/index.html',
  434.             ]
  435.     try:
  436.         for url in args:
  437.             print '-'*10, url, '-'*10
  438.             fn, h = urlretrieve(url)
  439.             print fn, h
  440.             if h:
  441.                 print '======'
  442.                 for k in h.keys(): print k + ':', h[k]
  443.                 print '======'
  444.             fp = open(fn, 'r')
  445.             data = fp.read()
  446.             del fp
  447.             print regsub.gsub('\r', '', data)
  448.             fn, h = None, None
  449.         print '-'*40
  450.     finally:
  451.         urlcleanup()
  452.  
  453. # Run test program when run as a script
  454. if __name__ == '__main__':
  455.     test1()
  456.     test()
  457.