You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1557 lines
55 KiB

4 years ago
  1. # Natural Language Toolkit: Utility functions
  2. #
  3. # Copyright (C) 2001-2019 NLTK Project
  4. # Author: Edward Loper <edloper@gmail.com>
  5. # URL: <http://nltk.org/>
  6. # For license information, see LICENSE.TXT
  7. """
  8. Functions to find and load NLTK resource files, such as corpora,
  9. grammars, and saved processing objects. Resource files are identified
  10. using URLs, such as ``nltk:corpora/abc/rural.txt`` or
  11. ``http://nltk.org/sample/toy.cfg``. The following URL protocols are
  12. supported:
  13. - ``file:path``: Specifies the file whose path is *path*.
  14. Both relative and absolute paths may be used.
  15. - ``http://host/path``: Specifies the file stored on the web
  16. server *host* at path *path*.
  17. - ``nltk:path``: Specifies the file stored in the NLTK data
  18. package at *path*. NLTK will search for these files in the
  19. directories specified by ``nltk.data.path``.
  20. If no protocol is specified, then the default protocol ``nltk:`` will
  21. be used.
  22. This module provides to functions that can be used to access a
  23. resource file, given its URL: ``load()`` loads a given resource, and
  24. adds it to a resource cache; and ``retrieve()`` copies a given resource
  25. to a local file.
  26. """
  27. from __future__ import print_function, unicode_literals, division
  28. import functools
  29. import textwrap
  30. import io
  31. import os
  32. import re
  33. import sys
  34. import zipfile
  35. import codecs
  36. from abc import ABCMeta, abstractmethod
  37. from gzip import GzipFile, WRITE as GZ_WRITE
  38. from six import add_metaclass
  39. from six import string_types, text_type
  40. from six.moves.urllib.request import urlopen, url2pathname
  41. try:
  42. import cPickle as pickle
  43. except ImportError:
  44. import pickle
  45. try: # Python 3.
  46. textwrap_indent = functools.partial(textwrap.indent, prefix=' ')
  47. except AttributeError: # Python 2; indent() not available for Python2.
  48. textwrap_fill = functools.partial(
  49. textwrap.fill,
  50. initial_indent=' ',
  51. subsequent_indent=' ',
  52. replace_whitespace=False,
  53. )
  54. def textwrap_indent(text):
  55. return '\n'.join(textwrap_fill(line) for line in text.splitlines())
  56. try:
  57. from zlib import Z_SYNC_FLUSH as FLUSH
  58. except ImportError:
  59. from zlib import Z_FINISH as FLUSH
  60. # this import should be more specific:
  61. import nltk
  62. from nltk.compat import py3_data, add_py3_data, BytesIO
  63. ######################################################################
  64. # Search Path
  65. ######################################################################
  66. path = []
  67. """A list of directories where the NLTK data package might reside.
  68. These directories will be checked in order when looking for a
  69. resource in the data package. Note that this allows users to
  70. substitute in their own versions of resources, if they have them
  71. (e.g., in their home directory under ~/nltk_data)."""
  72. # User-specified locations:
  73. _paths_from_env = os.environ.get('NLTK_DATA', str('')).split(os.pathsep)
  74. path += [d for d in _paths_from_env if d]
  75. if 'APPENGINE_RUNTIME' not in os.environ and os.path.expanduser('~/') != '~/':
  76. path.append(os.path.expanduser(str('~/nltk_data')))
  77. if sys.platform.startswith('win'):
  78. # Common locations on Windows:
  79. path += [
  80. os.path.join(sys.prefix, str('nltk_data')),
  81. os.path.join(sys.prefix, str('share'), str('nltk_data')),
  82. os.path.join(sys.prefix, str('lib'), str('nltk_data')),
  83. os.path.join(os.environ.get(str('APPDATA'), str('C:\\')), str('nltk_data')),
  84. str(r'C:\nltk_data'),
  85. str(r'D:\nltk_data'),
  86. str(r'E:\nltk_data'),
  87. ]
  88. else:
  89. # Common locations on UNIX & OS X:
  90. path += [
  91. os.path.join(sys.prefix, str('nltk_data')),
  92. os.path.join(sys.prefix, str('share'), str('nltk_data')),
  93. os.path.join(sys.prefix, str('lib'), str('nltk_data')),
  94. str('/usr/share/nltk_data'),
  95. str('/usr/local/share/nltk_data'),
  96. str('/usr/lib/nltk_data'),
  97. str('/usr/local/lib/nltk_data'),
  98. ]
  99. ######################################################################
  100. # Util Functions
  101. ######################################################################
  102. def gzip_open_unicode(
  103. filename,
  104. mode="rb",
  105. compresslevel=9,
  106. encoding='utf-8',
  107. fileobj=None,
  108. errors=None,
  109. newline=None,
  110. ):
  111. if fileobj is None:
  112. fileobj = GzipFile(filename, mode, compresslevel, fileobj)
  113. return io.TextIOWrapper(fileobj, encoding, errors, newline)
  114. def split_resource_url(resource_url):
  115. """
  116. Splits a resource url into "<protocol>:<path>".
  117. >>> windows = sys.platform.startswith('win')
  118. >>> split_resource_url('nltk:home/nltk')
  119. ('nltk', 'home/nltk')
  120. >>> split_resource_url('nltk:/home/nltk')
  121. ('nltk', '/home/nltk')
  122. >>> split_resource_url('file:/home/nltk')
  123. ('file', '/home/nltk')
  124. >>> split_resource_url('file:///home/nltk')
  125. ('file', '/home/nltk')
  126. >>> split_resource_url('file:///C:/home/nltk')
  127. ('file', '/C:/home/nltk')
  128. """
  129. protocol, path_ = resource_url.split(':', 1)
  130. if protocol == 'nltk':
  131. pass
  132. elif protocol == 'file':
  133. if path_.startswith('/'):
  134. path_ = '/' + path_.lstrip('/')
  135. else:
  136. path_ = re.sub(r'^/{0,2}', '', path_)
  137. return protocol, path_
  138. def normalize_resource_url(resource_url):
  139. r"""
  140. Normalizes a resource url
  141. >>> windows = sys.platform.startswith('win')
  142. >>> os.path.normpath(split_resource_url(normalize_resource_url('file:grammar.fcfg'))[1]) == \
  143. ... ('\\' if windows else '') + os.path.abspath(os.path.join(os.curdir, 'grammar.fcfg'))
  144. True
  145. >>> not windows or normalize_resource_url('file:C:/dir/file') == 'file:///C:/dir/file'
  146. True
  147. >>> not windows or normalize_resource_url('file:C:\\dir\\file') == 'file:///C:/dir/file'
  148. True
  149. >>> not windows or normalize_resource_url('file:C:\\dir/file') == 'file:///C:/dir/file'
  150. True
  151. >>> not windows or normalize_resource_url('file://C:/dir/file') == 'file:///C:/dir/file'
  152. True
  153. >>> not windows or normalize_resource_url('file:////C:/dir/file') == 'file:///C:/dir/file'
  154. True
  155. >>> not windows or normalize_resource_url('nltk:C:/dir/file') == 'file:///C:/dir/file'
  156. True
  157. >>> not windows or normalize_resource_url('nltk:C:\\dir\\file') == 'file:///C:/dir/file'
  158. True
  159. >>> windows or normalize_resource_url('file:/dir/file/toy.cfg') == 'file:///dir/file/toy.cfg'
  160. True
  161. >>> normalize_resource_url('nltk:home/nltk')
  162. 'nltk:home/nltk'
  163. >>> windows or normalize_resource_url('nltk:/home/nltk') == 'file:///home/nltk'
  164. True
  165. >>> normalize_resource_url('http://example.com/dir/file')
  166. 'http://example.com/dir/file'
  167. >>> normalize_resource_url('dir/file')
  168. 'nltk:dir/file'
  169. """
  170. try:
  171. protocol, name = split_resource_url(resource_url)
  172. except ValueError:
  173. # the resource url has no protocol, use the nltk protocol by default
  174. protocol = 'nltk'
  175. name = resource_url
  176. # use file protocol if the path is an absolute path
  177. if protocol == 'nltk' and os.path.isabs(name):
  178. protocol = 'file://'
  179. name = normalize_resource_name(name, False, None)
  180. elif protocol == 'file':
  181. protocol = 'file://'
  182. # name is absolute
  183. name = normalize_resource_name(name, False, None)
  184. elif protocol == 'nltk':
  185. protocol = 'nltk:'
  186. name = normalize_resource_name(name, True)
  187. else:
  188. # handled by urllib
  189. protocol += '://'
  190. return ''.join([protocol, name])
  191. def normalize_resource_name(resource_name, allow_relative=True, relative_path=None):
  192. """
  193. :type resource_name: str or unicode
  194. :param resource_name: The name of the resource to search for.
  195. Resource names are posix-style relative path names, such as
  196. ``corpora/brown``. Directory names will automatically
  197. be converted to a platform-appropriate path separator.
  198. Directory trailing slashes are preserved
  199. >>> windows = sys.platform.startswith('win')
  200. >>> normalize_resource_name('.', True)
  201. './'
  202. >>> normalize_resource_name('./', True)
  203. './'
  204. >>> windows or normalize_resource_name('dir/file', False, '/') == '/dir/file'
  205. True
  206. >>> not windows or normalize_resource_name('C:/file', False, '/') == '/C:/file'
  207. True
  208. >>> windows or normalize_resource_name('/dir/file', False, '/') == '/dir/file'
  209. True
  210. >>> windows or normalize_resource_name('../dir/file', False, '/') == '/dir/file'
  211. True
  212. >>> not windows or normalize_resource_name('/dir/file', True, '/') == 'dir/file'
  213. True
  214. >>> windows or normalize_resource_name('/dir/file', True, '/') == '/dir/file'
  215. True
  216. """
  217. is_dir = bool(re.search(r'[\\/.]$', resource_name)) or resource_name.endswith(
  218. os.path.sep
  219. )
  220. if sys.platform.startswith('win'):
  221. resource_name = resource_name.lstrip('/')
  222. else:
  223. resource_name = re.sub(r'^/+', '/', resource_name)
  224. if allow_relative:
  225. resource_name = os.path.normpath(resource_name)
  226. else:
  227. if relative_path is None:
  228. relative_path = os.curdir
  229. resource_name = os.path.abspath(os.path.join(relative_path, resource_name))
  230. resource_name = resource_name.replace('\\', '/').replace(os.path.sep, '/')
  231. if sys.platform.startswith('win') and os.path.isabs(resource_name):
  232. resource_name = '/' + resource_name
  233. if is_dir and not resource_name.endswith('/'):
  234. resource_name += '/'
  235. return resource_name
  236. ######################################################################
  237. # Path Pointers
  238. ######################################################################
  239. @add_metaclass(ABCMeta)
  240. class PathPointer(object):
  241. """
  242. An abstract base class for 'path pointers,' used by NLTK's data
  243. package to identify specific paths. Two subclasses exist:
  244. ``FileSystemPathPointer`` identifies a file that can be accessed
  245. directly via a given absolute path. ``ZipFilePathPointer``
  246. identifies a file contained within a zipfile, that can be accessed
  247. by reading that zipfile.
  248. """
  249. @abstractmethod
  250. def open(self, encoding=None):
  251. """
  252. Return a seekable read-only stream that can be used to read
  253. the contents of the file identified by this path pointer.
  254. :raise IOError: If the path specified by this pointer does
  255. not contain a readable file.
  256. """
  257. @abstractmethod
  258. def file_size(self):
  259. """
  260. Return the size of the file pointed to by this path pointer,
  261. in bytes.
  262. :raise IOError: If the path specified by this pointer does
  263. not contain a readable file.
  264. """
  265. @abstractmethod
  266. def join(self, fileid):
  267. """
  268. Return a new path pointer formed by starting at the path
  269. identified by this pointer, and then following the relative
  270. path given by ``fileid``. The path components of ``fileid``
  271. should be separated by forward slashes, regardless of
  272. the underlying file system's path seperator character.
  273. """
  274. class FileSystemPathPointer(PathPointer, text_type):
  275. """
  276. A path pointer that identifies a file which can be accessed
  277. directly via a given absolute path.
  278. """
  279. @py3_data
  280. def __init__(self, _path):
  281. """
  282. Create a new path pointer for the given absolute path.
  283. :raise IOError: If the given path does not exist.
  284. """
  285. _path = os.path.abspath(_path)
  286. if not os.path.exists(_path):
  287. raise IOError('No such file or directory: %r' % _path)
  288. self._path = _path
  289. # There's no need to call str.__init__(), since it's a no-op;
  290. # str does all of its setup work in __new__.
  291. @property
  292. def path(self):
  293. """The absolute path identified by this path pointer."""
  294. return self._path
  295. def open(self, encoding=None):
  296. stream = open(self._path, 'rb')
  297. if encoding is not None:
  298. stream = SeekableUnicodeStreamReader(stream, encoding)
  299. return stream
  300. def file_size(self):
  301. return os.stat(self._path).st_size
  302. def join(self, fileid):
  303. _path = os.path.join(self._path, fileid)
  304. return FileSystemPathPointer(_path)
  305. def __repr__(self):
  306. # This should be a byte string under Python 2.x;
  307. # we don't want transliteration here so
  308. # @python_2_unicode_compatible is not used.
  309. return str('FileSystemPathPointer(%r)' % self._path)
  310. def __str__(self):
  311. return self._path
  312. class BufferedGzipFile(GzipFile):
  313. """
  314. A ``GzipFile`` subclass that buffers calls to ``read()`` and ``write()``.
  315. This allows faster reads and writes of data to and from gzip-compressed
  316. files at the cost of using more memory.
  317. The default buffer size is 2MB.
  318. ``BufferedGzipFile`` is useful for loading large gzipped pickle objects
  319. as well as writing large encoded feature files for classifier training.
  320. """
  321. MB = 2 ** 20
  322. SIZE = 2 * MB
  323. @py3_data
  324. def __init__(
  325. self, filename=None, mode=None, compresslevel=9, fileobj=None, **kwargs
  326. ):
  327. """
  328. Return a buffered gzip file object.
  329. :param filename: a filesystem path
  330. :type filename: str
  331. :param mode: a file mode which can be any of 'r', 'rb', 'a', 'ab',
  332. 'w', or 'wb'
  333. :type mode: str
  334. :param compresslevel: The compresslevel argument is an integer from 1
  335. to 9 controlling the level of compression; 1 is fastest and
  336. produces the least compression, and 9 is slowest and produces the
  337. most compression. The default is 9.
  338. :type compresslevel: int
  339. :param fileobj: a BytesIO stream to read from instead of a file.
  340. :type fileobj: BytesIO
  341. :param size: number of bytes to buffer during calls to read() and write()
  342. :type size: int
  343. :rtype: BufferedGzipFile
  344. """
  345. GzipFile.__init__(self, filename, mode, compresslevel, fileobj)
  346. self._size = kwargs.get('size', self.SIZE)
  347. self._nltk_buffer = BytesIO()
  348. # cStringIO does not support len.
  349. self._len = 0
  350. def _reset_buffer(self):
  351. # For some reason calling BytesIO.truncate() here will lead to
  352. # inconsistent writes so just set _buffer to a new BytesIO object.
  353. self._nltk_buffer = BytesIO()
  354. self._len = 0
  355. def _write_buffer(self, data):
  356. # Simply write to the buffer and increment the buffer size.
  357. if data is not None:
  358. self._nltk_buffer.write(data)
  359. self._len += len(data)
  360. def _write_gzip(self, data):
  361. # Write the current buffer to the GzipFile.
  362. GzipFile.write(self, self._nltk_buffer.getvalue())
  363. # Then reset the buffer and write the new data to the buffer.
  364. self._reset_buffer()
  365. self._write_buffer(data)
  366. def close(self):
  367. # GzipFile.close() doesn't actuallly close anything.
  368. if self.mode == GZ_WRITE:
  369. self._write_gzip(None)
  370. self._reset_buffer()
  371. return GzipFile.close(self)
  372. def flush(self, lib_mode=FLUSH):
  373. self._nltk_buffer.flush()
  374. GzipFile.flush(self, lib_mode)
  375. def read(self, size=None):
  376. if not size:
  377. size = self._size
  378. contents = BytesIO()
  379. while True:
  380. blocks = GzipFile.read(self, size)
  381. if not blocks:
  382. contents.flush()
  383. break
  384. contents.write(blocks)
  385. return contents.getvalue()
  386. else:
  387. return GzipFile.read(self, size)
  388. def write(self, data, size=-1):
  389. """
  390. :param data: bytes to write to file or buffer
  391. :type data: bytes
  392. :param size: buffer at least size bytes before writing to file
  393. :type size: int
  394. """
  395. if not size:
  396. size = self._size
  397. if self._len + len(data) <= size:
  398. self._write_buffer(data)
  399. else:
  400. self._write_gzip(data)
  401. class GzipFileSystemPathPointer(FileSystemPathPointer):
  402. """
  403. A subclass of ``FileSystemPathPointer`` that identifies a gzip-compressed
  404. file located at a given absolute path. ``GzipFileSystemPathPointer`` is
  405. appropriate for loading large gzip-compressed pickle objects efficiently.
  406. """
  407. def open(self, encoding=None):
  408. # Note: In >= Python3.5, GzipFile is already using a
  409. # buffered reader in the backend which has a variable self._buffer
  410. # See https://github.com/nltk/nltk/issues/1308
  411. if sys.version.startswith('2.7') or sys.version.startswith('3.4'):
  412. stream = BufferedGzipFile(self._path, 'rb')
  413. else:
  414. stream = GzipFile(self._path, 'rb')
  415. if encoding:
  416. stream = SeekableUnicodeStreamReader(stream, encoding)
  417. return stream
  418. class ZipFilePathPointer(PathPointer):
  419. """
  420. A path pointer that identifies a file contained within a zipfile,
  421. which can be accessed by reading that zipfile.
  422. """
  423. @py3_data
  424. def __init__(self, zipfile, entry=''):
  425. """
  426. Create a new path pointer pointing at the specified entry
  427. in the given zipfile.
  428. :raise IOError: If the given zipfile does not exist, or if it
  429. does not contain the specified entry.
  430. """
  431. if isinstance(zipfile, string_types):
  432. zipfile = OpenOnDemandZipFile(os.path.abspath(zipfile))
  433. # Check that the entry exists:
  434. if entry:
  435. # Normalize the entry string, it should be relative:
  436. entry = normalize_resource_name(entry, True, '/').lstrip('/')
  437. try:
  438. zipfile.getinfo(entry)
  439. except Exception:
  440. # Sometimes directories aren't explicitly listed in
  441. # the zip file. So if `entry` is a directory name,
  442. # then check if the zipfile contains any files that
  443. # are under the given directory.
  444. if entry.endswith('/') and [
  445. n for n in zipfile.namelist() if n.startswith(entry)
  446. ]:
  447. pass # zipfile contains a file in that directory.
  448. else:
  449. # Otherwise, complain.
  450. raise IOError(
  451. 'Zipfile %r does not contain %r' % (zipfile.filename, entry)
  452. )
  453. self._zipfile = zipfile
  454. self._entry = entry
  455. @property
  456. def zipfile(self):
  457. """
  458. The zipfile.ZipFile object used to access the zip file
  459. containing the entry identified by this path pointer.
  460. """
  461. return self._zipfile
  462. @property
  463. def entry(self):
  464. """
  465. The name of the file within zipfile that this path
  466. pointer points to.
  467. """
  468. return self._entry
  469. def open(self, encoding=None):
  470. data = self._zipfile.read(self._entry)
  471. stream = BytesIO(data)
  472. if self._entry.endswith('.gz'):
  473. # Note: In >= Python3.5, GzipFile is already using a
  474. # buffered reader in the backend which has a variable self._buffer
  475. # See https://github.com/nltk/nltk/issues/1308
  476. if sys.version.startswith('2.7') or sys.version.startswith('3.4'):
  477. stream = BufferedGzipFile(self._entry, fileobj=stream)
  478. else:
  479. stream = GzipFile(self._entry, fileobj=stream)
  480. elif encoding is not None:
  481. stream = SeekableUnicodeStreamReader(stream, encoding)
  482. return stream
  483. def file_size(self):
  484. return self._zipfile.getinfo(self._entry).file_size
  485. def join(self, fileid):
  486. entry = '%s/%s' % (self._entry, fileid)
  487. return ZipFilePathPointer(self._zipfile, entry)
  488. def __repr__(self):
  489. return str('ZipFilePathPointer(%r, %r)') % (self._zipfile.filename, self._entry)
  490. def __str__(self):
  491. return os.path.normpath(os.path.join(self._zipfile.filename, self._entry))
  492. ######################################################################
  493. # Access Functions
  494. ######################################################################
  495. # Don't use a weak dictionary, because in the common case this
  496. # causes a lot more reloading that necessary.
  497. _resource_cache = {}
  498. """A dictionary used to cache resources so that they won't
  499. need to be loaded more than once."""
  500. def find(resource_name, paths=None):
  501. """
  502. Find the given resource by searching through the directories and
  503. zip files in paths, where a None or empty string specifies an absolute path.
  504. Returns a corresponding path name. If the given resource is not
  505. found, raise a ``LookupError``, whose message gives a pointer to
  506. the installation instructions for the NLTK downloader.
  507. Zip File Handling:
  508. - If ``resource_name`` contains a component with a ``.zip``
  509. extension, then it is assumed to be a zipfile; and the
  510. remaining path components are used to look inside the zipfile.
  511. - If any element of ``nltk.data.path`` has a ``.zip`` extension,
  512. then it is assumed to be a zipfile.
  513. - If a given resource name that does not contain any zipfile
  514. component is not found initially, then ``find()`` will make a
  515. second attempt to find that resource, by replacing each
  516. component *p* in the path with *p.zip/p*. For example, this
  517. allows ``find()`` to map the resource name
  518. ``corpora/chat80/cities.pl`` to a zip file path pointer to
  519. ``corpora/chat80.zip/chat80/cities.pl``.
  520. - When using ``find()`` to locate a directory contained in a
  521. zipfile, the resource name must end with the forward slash
  522. character. Otherwise, ``find()`` will not locate the
  523. directory.
  524. :type resource_name: str or unicode
  525. :param resource_name: The name of the resource to search for.
  526. Resource names are posix-style relative path names, such as
  527. ``corpora/brown``. Directory names will be
  528. automatically converted to a platform-appropriate path separator.
  529. :rtype: str
  530. """
  531. resource_name = normalize_resource_name(resource_name, True)
  532. # Resolve default paths at runtime in-case the user overrides
  533. # nltk.data.path
  534. if paths is None:
  535. paths = path
  536. # Check if the resource name includes a zipfile name
  537. m = re.match(r'(.*\.zip)/?(.*)$|', resource_name)
  538. zipfile, zipentry = m.groups()
  539. # Check each item in our path
  540. for path_ in paths:
  541. # Is the path item a zipfile?
  542. if path_ and (os.path.isfile(path_) and path_.endswith('.zip')):
  543. try:
  544. return ZipFilePathPointer(path_, resource_name)
  545. except IOError:
  546. # resource not in zipfile
  547. continue
  548. # Is the path item a directory or is resource_name an absolute path?
  549. elif not path_ or os.path.isdir(path_):
  550. if zipfile is None:
  551. p = os.path.join(path_, url2pathname(resource_name))
  552. if os.path.exists(p):
  553. if p.endswith('.gz'):
  554. return GzipFileSystemPathPointer(p)
  555. else:
  556. return FileSystemPathPointer(p)
  557. else:
  558. p = os.path.join(path_, url2pathname(zipfile))
  559. if os.path.exists(p):
  560. try:
  561. return ZipFilePathPointer(p, zipentry)
  562. except IOError:
  563. # resource not in zipfile
  564. continue
  565. # Fallback: if the path doesn't include a zip file, then try
  566. # again, assuming that one of the path components is inside a
  567. # zipfile of the same name.
  568. if zipfile is None:
  569. pieces = resource_name.split('/')
  570. for i in range(len(pieces)):
  571. modified_name = '/'.join(pieces[:i] + [pieces[i] + '.zip'] + pieces[i:])
  572. try:
  573. return find(modified_name, paths)
  574. except LookupError:
  575. pass
  576. # Identify the package (i.e. the .zip file) to download.
  577. resource_zipname = resource_name.split('/')[1]
  578. if resource_zipname.endswith('.zip'):
  579. resource_zipname = resource_zipname.rpartition('.')[0]
  580. # Display a friendly error message if the resource wasn't found:
  581. msg = str(
  582. "Resource \33[93m{resource}\033[0m not found.\n"
  583. "Please use the NLTK Downloader to obtain the resource:\n\n"
  584. "\33[31m" # To display red text in terminal.
  585. ">>> import nltk\n"
  586. ">>> nltk.download(\'{resource}\')\n"
  587. "\033[0m"
  588. ).format(resource=resource_zipname)
  589. msg = textwrap_indent(msg)
  590. msg += '\n For more information see: https://www.nltk.org/data.html\n'
  591. msg += '\n Attempted to load \33[93m{resource_name}\033[0m\n'.format(
  592. resource_name=resource_name
  593. )
  594. msg += '\n Searched in:' + ''.join('\n - %r' % d for d in paths)
  595. sep = '*' * 70
  596. resource_not_found = '\n%s\n%s\n%s\n' % (sep, msg, sep)
  597. raise LookupError(resource_not_found)
  598. def retrieve(resource_url, filename=None, verbose=True):
  599. """
  600. Copy the given resource to a local file. If no filename is
  601. specified, then use the URL's filename. If there is already a
  602. file named ``filename``, then raise a ``ValueError``.
  603. :type resource_url: str
  604. :param resource_url: A URL specifying where the resource should be
  605. loaded from. The default protocol is "nltk:", which searches
  606. for the file in the the NLTK data package.
  607. """
  608. resource_url = normalize_resource_url(resource_url)
  609. if filename is None:
  610. if resource_url.startswith('file:'):
  611. filename = os.path.split(resource_url)[-1]
  612. else:
  613. filename = re.sub(r'(^\w+:)?.*/', '', resource_url)
  614. if os.path.exists(filename):
  615. filename = os.path.abspath(filename)
  616. raise ValueError("File %r already exists!" % filename)
  617. if verbose:
  618. print('Retrieving %r, saving to %r' % (resource_url, filename))
  619. # Open the input & output streams.
  620. infile = _open(resource_url)
  621. # Copy infile -> outfile, using 64k blocks.
  622. with open(filename, "wb") as outfile:
  623. while True:
  624. s = infile.read(1024 * 64) # 64k blocks.
  625. outfile.write(s)
  626. if not s:
  627. break
  628. infile.close()
  629. #: A dictionary describing the formats that are supported by NLTK's
  630. #: load() method. Keys are format names, and values are format
  631. #: descriptions.
  632. FORMATS = {
  633. 'pickle': "A serialized python object, stored using the pickle module.",
  634. 'json': "A serialized python object, stored using the json module.",
  635. 'yaml': "A serialized python object, stored using the yaml module.",
  636. 'cfg': "A context free grammar.",
  637. 'pcfg': "A probabilistic CFG.",
  638. 'fcfg': "A feature CFG.",
  639. 'fol': "A list of first order logic expressions, parsed with "
  640. "nltk.sem.logic.Expression.fromstring.",
  641. 'logic': "A list of first order logic expressions, parsed with "
  642. "nltk.sem.logic.LogicParser. Requires an additional logic_parser "
  643. "parameter",
  644. 'val': "A semantic valuation, parsed by nltk.sem.Valuation.fromstring.",
  645. 'raw': "The raw (byte string) contents of a file.",
  646. 'text': "The raw (unicode string) contents of a file. ",
  647. }
  648. #: A dictionary mapping from file extensions to format names, used
  649. #: by load() when format="auto" to decide the format for a
  650. #: given resource url.
  651. AUTO_FORMATS = {
  652. 'pickle': 'pickle',
  653. 'json': 'json',
  654. 'yaml': 'yaml',
  655. 'cfg': 'cfg',
  656. 'pcfg': 'pcfg',
  657. 'fcfg': 'fcfg',
  658. 'fol': 'fol',
  659. 'logic': 'logic',
  660. 'val': 'val',
  661. 'txt': 'text',
  662. 'text': 'text',
  663. }
  664. def load(
  665. resource_url,
  666. format='auto',
  667. cache=True,
  668. verbose=False,
  669. logic_parser=None,
  670. fstruct_reader=None,
  671. encoding=None,
  672. ):
  673. """
  674. Load a given resource from the NLTK data package. The following
  675. resource formats are currently supported:
  676. - ``pickle``
  677. - ``json``
  678. - ``yaml``
  679. - ``cfg`` (context free grammars)
  680. - ``pcfg`` (probabilistic CFGs)
  681. - ``fcfg`` (feature-based CFGs)
  682. - ``fol`` (formulas of First Order Logic)
  683. - ``logic`` (Logical formulas to be parsed by the given logic_parser)
  684. - ``val`` (valuation of First Order Logic model)
  685. - ``text`` (the file contents as a unicode string)
  686. - ``raw`` (the raw file contents as a byte string)
  687. If no format is specified, ``load()`` will attempt to determine a
  688. format based on the resource name's file extension. If that
  689. fails, ``load()`` will raise a ``ValueError`` exception.
  690. For all text formats (everything except ``pickle``, ``json``, ``yaml`` and ``raw``),
  691. it tries to decode the raw contents using UTF-8, and if that doesn't
  692. work, it tries with ISO-8859-1 (Latin-1), unless the ``encoding``
  693. is specified.
  694. :type resource_url: str
  695. :param resource_url: A URL specifying where the resource should be
  696. loaded from. The default protocol is "nltk:", which searches
  697. for the file in the the NLTK data package.
  698. :type cache: bool
  699. :param cache: If true, add this resource to a cache. If load()
  700. finds a resource in its cache, then it will return it from the
  701. cache rather than loading it. The cache uses weak references,
  702. so a resource wil automatically be expunged from the cache
  703. when no more objects are using it.
  704. :type verbose: bool
  705. :param verbose: If true, print a message when loading a resource.
  706. Messages are not displayed when a resource is retrieved from
  707. the cache.
  708. :type logic_parser: LogicParser
  709. :param logic_parser: The parser that will be used to parse logical
  710. expressions.
  711. :type fstruct_reader: FeatStructReader
  712. :param fstruct_reader: The parser that will be used to parse the
  713. feature structure of an fcfg.
  714. :type encoding: str
  715. :param encoding: the encoding of the input; only used for text formats.
  716. """
  717. resource_url = normalize_resource_url(resource_url)
  718. resource_url = add_py3_data(resource_url)
  719. # Determine the format of the resource.
  720. if format == 'auto':
  721. resource_url_parts = resource_url.split('.')
  722. ext = resource_url_parts[-1]
  723. if ext == 'gz':
  724. ext = resource_url_parts[-2]
  725. format = AUTO_FORMATS.get(ext)
  726. if format is None:
  727. raise ValueError(
  728. 'Could not determine format for %s based '
  729. 'on its file\nextension; use the "format" '
  730. 'argument to specify the format explicitly.' % resource_url
  731. )
  732. if format not in FORMATS:
  733. raise ValueError('Unknown format type: %s!' % (format,))
  734. # If we've cached the resource, then just return it.
  735. if cache:
  736. resource_val = _resource_cache.get((resource_url, format))
  737. if resource_val is not None:
  738. if verbose:
  739. print('<<Using cached copy of %s>>' % (resource_url,))
  740. return resource_val
  741. # Let the user know what's going on.
  742. if verbose:
  743. print('<<Loading %s>>' % (resource_url,))
  744. # Load the resource.
  745. opened_resource = _open(resource_url)
  746. if format == 'raw':
  747. resource_val = opened_resource.read()
  748. elif format == 'pickle':
  749. resource_val = pickle.load(opened_resource)
  750. elif format == 'json':
  751. import json
  752. from nltk.jsontags import json_tags
  753. resource_val = json.load(opened_resource)
  754. tag = None
  755. if len(resource_val) != 1:
  756. tag = next(resource_val.keys())
  757. if tag not in json_tags:
  758. raise ValueError('Unknown json tag.')
  759. elif format == 'yaml':
  760. import yaml
  761. resource_val = yaml.load(opened_resource)
  762. else:
  763. # The resource is a text format.
  764. binary_data = opened_resource.read()
  765. if encoding is not None:
  766. string_data = binary_data.decode(encoding)
  767. else:
  768. try:
  769. string_data = binary_data.decode('utf-8')
  770. except UnicodeDecodeError:
  771. string_data = binary_data.decode('latin-1')
  772. if format == 'text':
  773. resource_val = string_data
  774. elif format == 'cfg':
  775. resource_val = nltk.grammar.CFG.fromstring(string_data, encoding=encoding)
  776. elif format == 'pcfg':
  777. resource_val = nltk.grammar.PCFG.fromstring(string_data, encoding=encoding)
  778. elif format == 'fcfg':
  779. resource_val = nltk.grammar.FeatureGrammar.fromstring(
  780. string_data,
  781. logic_parser=logic_parser,
  782. fstruct_reader=fstruct_reader,
  783. encoding=encoding,
  784. )
  785. elif format == 'fol':
  786. resource_val = nltk.sem.read_logic(
  787. string_data,
  788. logic_parser=nltk.sem.logic.LogicParser(),
  789. encoding=encoding,
  790. )
  791. elif format == 'logic':
  792. resource_val = nltk.sem.read_logic(
  793. string_data, logic_parser=logic_parser, encoding=encoding
  794. )
  795. elif format == 'val':
  796. resource_val = nltk.sem.read_valuation(string_data, encoding=encoding)
  797. else:
  798. raise AssertionError(
  799. "Internal NLTK error: Format %s isn't "
  800. "handled by nltk.data.load()" % (format,)
  801. )
  802. opened_resource.close()
  803. # If requested, add it to the cache.
  804. if cache:
  805. try:
  806. _resource_cache[(resource_url, format)] = resource_val
  807. # TODO: add this line
  808. # print('<<Caching a copy of %s>>' % (resource_url,))
  809. except TypeError:
  810. # We can't create weak references to some object types, like
  811. # strings and tuples. For now, just don't cache them.
  812. pass
  813. return resource_val
  814. def show_cfg(resource_url, escape='##'):
  815. """
  816. Write out a grammar file, ignoring escaped and empty lines.
  817. :type resource_url: str
  818. :param resource_url: A URL specifying where the resource should be
  819. loaded from. The default protocol is "nltk:", which searches
  820. for the file in the the NLTK data package.
  821. :type escape: str
  822. :param escape: Prepended string that signals lines to be ignored
  823. """
  824. resource_url = normalize_resource_url(resource_url)
  825. resource_val = load(resource_url, format='text', cache=False)
  826. lines = resource_val.splitlines()
  827. for l in lines:
  828. if l.startswith(escape):
  829. continue
  830. if re.match('^$', l):
  831. continue
  832. print(l)
  833. def clear_cache():
  834. """
  835. Remove all objects from the resource cache.
  836. :see: load()
  837. """
  838. _resource_cache.clear()
  839. def _open(resource_url):
  840. """
  841. Helper function that returns an open file object for a resource,
  842. given its resource URL. If the given resource URL uses the "nltk:"
  843. protocol, or uses no protocol, then use ``nltk.data.find`` to find
  844. its path, and open it with the given mode; if the resource URL
  845. uses the 'file' protocol, then open the file with the given mode;
  846. otherwise, delegate to ``urllib2.urlopen``.
  847. :type resource_url: str
  848. :param resource_url: A URL specifying where the resource should be
  849. loaded from. The default protocol is "nltk:", which searches
  850. for the file in the the NLTK data package.
  851. """
  852. resource_url = normalize_resource_url(resource_url)
  853. protocol, path_ = split_resource_url(resource_url)
  854. if protocol is None or protocol.lower() == 'nltk':
  855. return find(path_, path + ['']).open()
  856. elif protocol.lower() == 'file':
  857. # urllib might not use mode='rb', so handle this one ourselves:
  858. return find(path_, ['']).open()
  859. else:
  860. return urlopen(resource_url)
  861. ######################################################################
  862. # Lazy Resource Loader
  863. ######################################################################
  864. # We shouldn't apply @python_2_unicode_compatible
  865. # decorator to LazyLoader, this is resource.__class__ responsibility.
  866. class LazyLoader(object):
  867. @py3_data
  868. def __init__(self, _path):
  869. self._path = _path
  870. def __load(self):
  871. resource = load(self._path)
  872. # This is where the magic happens! Transform ourselves into
  873. # the object by modifying our own __dict__ and __class__ to
  874. # match that of `resource`.
  875. self.__dict__ = resource.__dict__
  876. self.__class__ = resource.__class__
  877. def __getattr__(self, attr):
  878. self.__load()
  879. # This looks circular, but its not, since __load() changes our
  880. # __class__ to something new:
  881. return getattr(self, attr)
  882. def __repr__(self):
  883. self.__load()
  884. # This looks circular, but its not, since __load() changes our
  885. # __class__ to something new:
  886. return repr(self)
  887. ######################################################################
  888. # Open-On-Demand ZipFile
  889. ######################################################################
  890. class OpenOnDemandZipFile(zipfile.ZipFile):
  891. """
  892. A subclass of ``zipfile.ZipFile`` that closes its file pointer
  893. whenever it is not using it; and re-opens it when it needs to read
  894. data from the zipfile. This is useful for reducing the number of
  895. open file handles when many zip files are being accessed at once.
  896. ``OpenOnDemandZipFile`` must be constructed from a filename, not a
  897. file-like object (to allow re-opening). ``OpenOnDemandZipFile`` is
  898. read-only (i.e. ``write()`` and ``writestr()`` are disabled.
  899. """
  900. @py3_data
  901. def __init__(self, filename):
  902. if not isinstance(filename, string_types):
  903. raise TypeError('ReopenableZipFile filename must be a string')
  904. zipfile.ZipFile.__init__(self, filename)
  905. assert self.filename == filename
  906. self.close()
  907. # After closing a ZipFile object, the _fileRefCnt needs to be cleared
  908. # for Python2and3 compatible code.
  909. self._fileRefCnt = 0
  910. def read(self, name):
  911. assert self.fp is None
  912. self.fp = open(self.filename, 'rb')
  913. value = zipfile.ZipFile.read(self, name)
  914. # Ensure that _fileRefCnt needs to be set for Python2and3 compatible code.
  915. # Since we only opened one file here, we add 1.
  916. self._fileRefCnt += 1
  917. self.close()
  918. return value
  919. def write(self, *args, **kwargs):
  920. """:raise NotImplementedError: OpenOnDemandZipfile is read-only"""
  921. raise NotImplementedError('OpenOnDemandZipfile is read-only')
  922. def writestr(self, *args, **kwargs):
  923. """:raise NotImplementedError: OpenOnDemandZipfile is read-only"""
  924. raise NotImplementedError('OpenOnDemandZipfile is read-only')
  925. def __repr__(self):
  926. return repr(str('OpenOnDemandZipFile(%r)') % self.filename)
  927. ######################################################################
  928. # { Seekable Unicode Stream Reader
  929. ######################################################################
  930. class SeekableUnicodeStreamReader(object):
  931. """
  932. A stream reader that automatically encodes the source byte stream
  933. into unicode (like ``codecs.StreamReader``); but still supports the
  934. ``seek()`` and ``tell()`` operations correctly. This is in contrast
  935. to ``codecs.StreamReader``, which provide *broken* ``seek()`` and
  936. ``tell()`` methods.
  937. This class was motivated by ``StreamBackedCorpusView``, which
  938. makes extensive use of ``seek()`` and ``tell()``, and needs to be
  939. able to handle unicode-encoded files.
  940. Note: this class requires stateless decoders. To my knowledge,
  941. this shouldn't cause a problem with any of python's builtin
  942. unicode encodings.
  943. """
  944. DEBUG = True # : If true, then perform extra sanity checks.
  945. @py3_data
  946. def __init__(self, stream, encoding, errors='strict'):
  947. # Rewind the stream to its beginning.
  948. stream.seek(0)
  949. self.stream = stream
  950. """The underlying stream."""
  951. self.encoding = encoding
  952. """The name of the encoding that should be used to encode the
  953. underlying stream."""
  954. self.errors = errors
  955. """The error mode that should be used when decoding data from
  956. the underlying stream. Can be 'strict', 'ignore', or
  957. 'replace'."""
  958. self.decode = codecs.getdecoder(encoding)
  959. """The function that is used to decode byte strings into
  960. unicode strings."""
  961. self.bytebuffer = b''
  962. """A buffer to use bytes that have been read but have not yet
  963. been decoded. This is only used when the final bytes from
  964. a read do not form a complete encoding for a character."""
  965. self.linebuffer = None
  966. """A buffer used by ``readline()`` to hold characters that have
  967. been read, but have not yet been returned by ``read()`` or
  968. ``readline()``. This buffer consists of a list of unicode
  969. strings, where each string corresponds to a single line.
  970. The final element of the list may or may not be a complete
  971. line. Note that the existence of a linebuffer makes the
  972. ``tell()`` operation more complex, because it must backtrack
  973. to the beginning of the buffer to determine the correct
  974. file position in the underlying byte stream."""
  975. self._rewind_checkpoint = 0
  976. """The file position at which the most recent read on the
  977. underlying stream began. This is used, together with
  978. ``_rewind_numchars``, to backtrack to the beginning of
  979. ``linebuffer`` (which is required by ``tell()``)."""
  980. self._rewind_numchars = None
  981. """The number of characters that have been returned since the
  982. read that started at ``_rewind_checkpoint``. This is used,
  983. together with ``_rewind_checkpoint``, to backtrack to the
  984. beginning of ``linebuffer`` (which is required by ``tell()``)."""
  985. self._bom = self._check_bom()
  986. """The length of the byte order marker at the beginning of
  987. the stream (or None for no byte order marker)."""
  988. # /////////////////////////////////////////////////////////////////
  989. # Read methods
  990. # /////////////////////////////////////////////////////////////////
  991. def read(self, size=None):
  992. """
  993. Read up to ``size`` bytes, decode them using this reader's
  994. encoding, and return the resulting unicode string.
  995. :param size: The maximum number of bytes to read. If not
  996. specified, then read as many bytes as possible.
  997. :type size: int
  998. :rtype: unicode
  999. """
  1000. chars = self._read(size)
  1001. # If linebuffer is not empty, then include it in the result
  1002. if self.linebuffer:
  1003. chars = ''.join(self.linebuffer) + chars
  1004. self.linebuffer = None
  1005. self._rewind_numchars = None
  1006. return chars
  1007. def discard_line(self):
  1008. if self.linebuffer and len(self.linebuffer) > 1:
  1009. line = self.linebuffer.pop(0)
  1010. self._rewind_numchars += len(line)
  1011. else:
  1012. self.stream.readline()
  1013. def readline(self, size=None):
  1014. """
  1015. Read a line of text, decode it using this reader's encoding,
  1016. and return the resulting unicode string.
  1017. :param size: The maximum number of bytes to read. If no
  1018. newline is encountered before ``size`` bytes have been read,
  1019. then the returned value may not be a complete line of text.
  1020. :type size: int
  1021. """
  1022. # If we have a non-empty linebuffer, then return the first
  1023. # line from it. (Note that the last element of linebuffer may
  1024. # not be a complete line; so let _read() deal with it.)
  1025. if self.linebuffer and len(self.linebuffer) > 1:
  1026. line = self.linebuffer.pop(0)
  1027. self._rewind_numchars += len(line)
  1028. return line
  1029. readsize = size or 72
  1030. chars = ''
  1031. # If there's a remaining incomplete line in the buffer, add it.
  1032. if self.linebuffer:
  1033. chars += self.linebuffer.pop()
  1034. self.linebuffer = None
  1035. while True:
  1036. startpos = self.stream.tell() - len(self.bytebuffer)
  1037. new_chars = self._read(readsize)
  1038. # If we're at a '\r', then read one extra character, since
  1039. # it might be a '\n', to get the proper line ending.
  1040. if new_chars and new_chars.endswith('\r'):
  1041. new_chars += self._read(1)
  1042. chars += new_chars
  1043. lines = chars.splitlines(True)
  1044. if len(lines) > 1:
  1045. line = lines[0]
  1046. self.linebuffer = lines[1:]
  1047. self._rewind_numchars = len(new_chars) - (len(chars) - len(line))
  1048. self._rewind_checkpoint = startpos
  1049. break
  1050. elif len(lines) == 1:
  1051. line0withend = lines[0]
  1052. line0withoutend = lines[0].splitlines(False)[0]
  1053. if line0withend != line0withoutend: # complete line
  1054. line = line0withend
  1055. break
  1056. if not new_chars or size is not None:
  1057. line = chars
  1058. break
  1059. # Read successively larger blocks of text.
  1060. if readsize < 8000:
  1061. readsize *= 2
  1062. return line
  1063. def readlines(self, sizehint=None, keepends=True):
  1064. """
  1065. Read this file's contents, decode them using this reader's
  1066. encoding, and return it as a list of unicode lines.
  1067. :rtype: list(unicode)
  1068. :param sizehint: Ignored.
  1069. :param keepends: If false, then strip newlines.
  1070. """
  1071. return self.read().splitlines(keepends)
  1072. def next(self):
  1073. """Return the next decoded line from the underlying stream."""
  1074. line = self.readline()
  1075. if line:
  1076. return line
  1077. else:
  1078. raise StopIteration
  1079. def __next__(self):
  1080. return self.next()
  1081. def __iter__(self):
  1082. """Return self"""
  1083. return self
  1084. def __del__(self):
  1085. # let garbage collector deal with still opened streams
  1086. if not self.closed:
  1087. self.close()
  1088. def xreadlines(self):
  1089. """Return self"""
  1090. return self
  1091. # /////////////////////////////////////////////////////////////////
  1092. # Pass-through methods & properties
  1093. # /////////////////////////////////////////////////////////////////
  1094. @property
  1095. def closed(self):
  1096. """True if the underlying stream is closed."""
  1097. return self.stream.closed
  1098. @property
  1099. def name(self):
  1100. """The name of the underlying stream."""
  1101. return self.stream.name
  1102. @property
  1103. def mode(self):
  1104. """The mode of the underlying stream."""
  1105. return self.stream.mode
  1106. def close(self):
  1107. """
  1108. Close the underlying stream.
  1109. """
  1110. self.stream.close()
  1111. # /////////////////////////////////////////////////////////////////
  1112. # Seek and tell
  1113. # /////////////////////////////////////////////////////////////////
  1114. def seek(self, offset, whence=0):
  1115. """
  1116. Move the stream to a new file position. If the reader is
  1117. maintaining any buffers, then they will be cleared.
  1118. :param offset: A byte count offset.
  1119. :param whence: If 0, then the offset is from the start of the file
  1120. (offset should be positive), if 1, then the offset is from the
  1121. current position (offset may be positive or negative); and if 2,
  1122. then the offset is from the end of the file (offset should
  1123. typically be negative).
  1124. """
  1125. if whence == 1:
  1126. raise ValueError(
  1127. 'Relative seek is not supported for '
  1128. 'SeekableUnicodeStreamReader -- consider '
  1129. 'using char_seek_forward() instead.'
  1130. )
  1131. self.stream.seek(offset, whence)
  1132. self.linebuffer = None
  1133. self.bytebuffer = b''
  1134. self._rewind_numchars = None
  1135. self._rewind_checkpoint = self.stream.tell()
  1136. def char_seek_forward(self, offset):
  1137. """
  1138. Move the read pointer forward by ``offset`` characters.
  1139. """
  1140. if offset < 0:
  1141. raise ValueError('Negative offsets are not supported')
  1142. # Clear all buffers.
  1143. self.seek(self.tell())
  1144. # Perform the seek operation.
  1145. self._char_seek_forward(offset)
  1146. def _char_seek_forward(self, offset, est_bytes=None):
  1147. """
  1148. Move the file position forward by ``offset`` characters,
  1149. ignoring all buffers.
  1150. :param est_bytes: A hint, giving an estimate of the number of
  1151. bytes that will be needed to move forward by ``offset`` chars.
  1152. Defaults to ``offset``.
  1153. """
  1154. if est_bytes is None:
  1155. est_bytes = offset
  1156. bytes = b''
  1157. while True:
  1158. # Read in a block of bytes.
  1159. newbytes = self.stream.read(est_bytes - len(bytes))
  1160. bytes += newbytes
  1161. # Decode the bytes to characters.
  1162. chars, bytes_decoded = self._incr_decode(bytes)
  1163. # If we got the right number of characters, then seek
  1164. # backwards over any truncated characters, and return.
  1165. if len(chars) == offset:
  1166. self.stream.seek(-len(bytes) + bytes_decoded, 1)
  1167. return
  1168. # If we went too far, then we can back-up until we get it
  1169. # right, using the bytes we've already read.
  1170. if len(chars) > offset:
  1171. while len(chars) > offset:
  1172. # Assume at least one byte/char.
  1173. est_bytes += offset - len(chars)
  1174. chars, bytes_decoded = self._incr_decode(bytes[:est_bytes])
  1175. self.stream.seek(-len(bytes) + bytes_decoded, 1)
  1176. return
  1177. # Otherwise, we haven't read enough bytes yet; loop again.
  1178. est_bytes += offset - len(chars)
  1179. def tell(self):
  1180. """
  1181. Return the current file position on the underlying byte
  1182. stream. If this reader is maintaining any buffers, then the
  1183. returned file position will be the position of the beginning
  1184. of those buffers.
  1185. """
  1186. # If nothing's buffered, then just return our current filepos:
  1187. if self.linebuffer is None:
  1188. return self.stream.tell() - len(self.bytebuffer)
  1189. # Otherwise, we'll need to backtrack the filepos until we
  1190. # reach the beginning of the buffer.
  1191. # Store our original file position, so we can return here.
  1192. orig_filepos = self.stream.tell()
  1193. # Calculate an estimate of where we think the newline is.
  1194. bytes_read = (orig_filepos - len(self.bytebuffer)) - self._rewind_checkpoint
  1195. buf_size = sum(len(line) for line in self.linebuffer)
  1196. est_bytes = int(
  1197. (bytes_read * self._rewind_numchars / (self._rewind_numchars + buf_size))
  1198. )
  1199. self.stream.seek(self._rewind_checkpoint)
  1200. self._char_seek_forward(self._rewind_numchars, est_bytes)
  1201. filepos = self.stream.tell()
  1202. # Sanity check
  1203. if self.DEBUG:
  1204. self.stream.seek(filepos)
  1205. check1 = self._incr_decode(self.stream.read(50))[0]
  1206. check2 = ''.join(self.linebuffer)
  1207. assert check1.startswith(check2) or check2.startswith(check1)
  1208. # Return to our original filepos (so we don't have to throw
  1209. # out our buffer.)
  1210. self.stream.seek(orig_filepos)
  1211. # Return the calculated filepos
  1212. return filepos
  1213. # /////////////////////////////////////////////////////////////////
  1214. # Helper methods
  1215. # /////////////////////////////////////////////////////////////////
  1216. def _read(self, size=None):
  1217. """
  1218. Read up to ``size`` bytes from the underlying stream, decode
  1219. them using this reader's encoding, and return the resulting
  1220. unicode string. ``linebuffer`` is not included in the result.
  1221. """
  1222. if size == 0:
  1223. return ''
  1224. # Skip past the byte order marker, if present.
  1225. if self._bom and self.stream.tell() == 0:
  1226. self.stream.read(self._bom)
  1227. # Read the requested number of bytes.
  1228. if size is None:
  1229. new_bytes = self.stream.read()
  1230. else:
  1231. new_bytes = self.stream.read(size)
  1232. bytes = self.bytebuffer + new_bytes
  1233. # Decode the bytes into unicode characters
  1234. chars, bytes_decoded = self._incr_decode(bytes)
  1235. # If we got bytes but couldn't decode any, then read further.
  1236. if (size is not None) and (not chars) and (len(new_bytes) > 0):
  1237. while not chars:
  1238. new_bytes = self.stream.read(1)
  1239. if not new_bytes:
  1240. break # end of file.
  1241. bytes += new_bytes
  1242. chars, bytes_decoded = self._incr_decode(bytes)
  1243. # Record any bytes we didn't consume.
  1244. self.bytebuffer = bytes[bytes_decoded:]
  1245. # Return the result
  1246. return chars
  1247. def _incr_decode(self, bytes):
  1248. """
  1249. Decode the given byte string into a unicode string, using this
  1250. reader's encoding. If an exception is encountered that
  1251. appears to be caused by a truncation error, then just decode
  1252. the byte string without the bytes that cause the trunctaion
  1253. error.
  1254. Return a tuple ``(chars, num_consumed)``, where ``chars`` is
  1255. the decoded unicode string, and ``num_consumed`` is the
  1256. number of bytes that were consumed.
  1257. """
  1258. while True:
  1259. try:
  1260. return self.decode(bytes, 'strict')
  1261. except UnicodeDecodeError as exc:
  1262. # If the exception occurs at the end of the string,
  1263. # then assume that it's a truncation error.
  1264. if exc.end == len(bytes):
  1265. return self.decode(bytes[: exc.start], self.errors)
  1266. # Otherwise, if we're being strict, then raise it.
  1267. elif self.errors == 'strict':
  1268. raise
  1269. # If we're not strict, then re-process it with our
  1270. # errors setting. This *may* raise an exception.
  1271. else:
  1272. return self.decode(bytes, self.errors)
  1273. _BOM_TABLE = {
  1274. 'utf8': [(codecs.BOM_UTF8, None)],
  1275. 'utf16': [(codecs.BOM_UTF16_LE, 'utf16-le'), (codecs.BOM_UTF16_BE, 'utf16-be')],
  1276. 'utf16le': [(codecs.BOM_UTF16_LE, None)],
  1277. 'utf16be': [(codecs.BOM_UTF16_BE, None)],
  1278. 'utf32': [(codecs.BOM_UTF32_LE, 'utf32-le'), (codecs.BOM_UTF32_BE, 'utf32-be')],
  1279. 'utf32le': [(codecs.BOM_UTF32_LE, None)],
  1280. 'utf32be': [(codecs.BOM_UTF32_BE, None)],
  1281. }
  1282. def _check_bom(self):
  1283. # Normalize our encoding name
  1284. enc = re.sub('[ -]', '', self.encoding.lower())
  1285. # Look up our encoding in the BOM table.
  1286. bom_info = self._BOM_TABLE.get(enc)
  1287. if bom_info:
  1288. # Read a prefix, to check against the BOM(s)
  1289. bytes = self.stream.read(16)
  1290. self.stream.seek(0)
  1291. # Check for each possible BOM.
  1292. for (bom, new_encoding) in bom_info:
  1293. if bytes.startswith(bom):
  1294. if new_encoding:
  1295. self.encoding = new_encoding
  1296. return len(bom)
  1297. return None
  1298. __all__ = [
  1299. 'path',
  1300. 'PathPointer',
  1301. 'FileSystemPathPointer',
  1302. 'BufferedGzipFile',
  1303. 'GzipFileSystemPathPointer',
  1304. 'GzipFileSystemPathPointer',
  1305. 'find',
  1306. 'retrieve',
  1307. 'FORMATS',
  1308. 'AUTO_FORMATS',
  1309. 'load',
  1310. 'show_cfg',
  1311. 'clear_cache',
  1312. 'LazyLoader',
  1313. 'OpenOnDemandZipFile',
  1314. 'GzipFileSystemPathPointer',
  1315. 'SeekableUnicodeStreamReader',
  1316. ]