ntriples.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. """
  2. N-Triples Parser
  3. License: GPL 2, W3C, BSD, or MIT
  4. Author: Sean B. Palmer, inamidst.com
  5. """
  6. from __future__ import annotations
  7. import codecs
  8. import re
  9. from io import BytesIO, StringIO, TextIOBase
  10. from typing import (
  11. IO,
  12. TYPE_CHECKING,
  13. Any,
  14. Match,
  15. MutableMapping,
  16. Optional,
  17. Pattern,
  18. TextIO,
  19. Union,
  20. )
  21. from rdflib.compat import _string_escape_map, decodeUnicodeEscape
  22. from rdflib.exceptions import ParserError as ParseError
  23. from rdflib.parser import InputSource, Parser
  24. from rdflib.term import BNode as bNode
  25. from rdflib.term import Literal, URIRef
  26. from rdflib.term import URIRef as URI # noqa: N814
  27. if TYPE_CHECKING:
  28. import typing_extensions as te
  29. from rdflib.graph import Graph, _ObjectType, _PredicateType, _SubjectType
  30. __all__ = [
  31. "unquote",
  32. "uriquote",
  33. "W3CNTriplesParser",
  34. "NTGraphSink",
  35. "NTParser",
  36. "DummySink",
  37. ]
  38. uriref = r'<([^:]+:[^\s"<>]*)>'
  39. literal = r'"([^"\\]*(?:\\.[^"\\]*)*)"'
  40. litinfo = r"(?:@([a-zA-Z]+(?:-[a-zA-Z0-9]+)*)|\^\^" + uriref + r")?"
  41. r_line = re.compile(r"([^\r\n]*)(?:\r\n|\r|\n)")
  42. r_wspace = re.compile(r"[ \t]*")
  43. r_wspaces = re.compile(r"[ \t]+")
  44. r_tail = re.compile(r"[ \t]*\.[ \t]*(#.*)?")
  45. r_uriref = re.compile(uriref)
  46. r_nodeid = re.compile(r"_:([A-Za-z0-9_:]([-A-Za-z0-9_:\.]*[-A-Za-z0-9_:])?)")
  47. r_literal = re.compile(literal + litinfo)
  48. bufsiz = 2048
  49. validate = False
  50. class DummySink:
  51. def __init__(self):
  52. self.length = 0
  53. def triple(self, s, p, o):
  54. self.length += 1
  55. print(s, p, o)
  56. r_safe = re.compile(r"([\x20\x21\x23-\x5B\x5D-\x7E]+)")
  57. r_quot = re.compile(r"""\\([tbnrf"'\\])""")
  58. r_uniquot = re.compile(r"\\u([0-9A-Fa-f]{4})|\\U([0-9A-Fa-f]{8})")
  59. def unquote(s: str) -> str:
  60. """Unquote an N-Triples string."""
  61. if not validate:
  62. if isinstance(s, str): # nquads
  63. s = decodeUnicodeEscape(s)
  64. else:
  65. s = s.decode("unicode-escape") # type: ignore[unreachable]
  66. return s
  67. else:
  68. result = []
  69. while s:
  70. m = r_safe.match(s)
  71. if m:
  72. s = s[m.end() :]
  73. result.append(m.group(1))
  74. continue
  75. m = r_quot.match(s)
  76. if m:
  77. s = s[2:]
  78. result.append(_string_escape_map[m.group(1)])
  79. continue
  80. m = r_uniquot.match(s)
  81. if m:
  82. s = s[m.end() :]
  83. u, U = m.groups() # noqa: N806
  84. codepoint = int(u or U, 16)
  85. if codepoint > 0x10FFFF:
  86. raise ParseError("Disallowed codepoint: %08X" % codepoint)
  87. result.append(chr(codepoint))
  88. elif s.startswith("\\"):
  89. raise ParseError("Illegal escape at: %s..." % s[:10])
  90. else:
  91. raise ParseError("Illegal literal character: %r" % s[0])
  92. return "".join(result)
  93. r_hibyte = re.compile(r"([\x80-\xFF])")
  94. def uriquote(uri: str) -> str:
  95. if not validate:
  96. return uri
  97. else:
  98. return r_hibyte.sub(lambda m: "%%%02X" % ord(m.group(1)), uri)
  99. _BNodeContextType = MutableMapping[str, bNode]
  100. class W3CNTriplesParser:
  101. """An N-Triples Parser.
  102. This is a legacy-style Triples parser for NTriples provided by W3C
  103. Example:
  104. ```python
  105. p = W3CNTriplesParser(sink=MySink())
  106. sink = p.parse(f) # file; use parsestring for a string
  107. ```
  108. To define a context in which blank node identifiers refer to the same blank node
  109. across instances of NTriplesParser, pass the same dict as `bnode_context` to each
  110. instance. By default, a new blank node context is created for each instance of
  111. `W3CNTriplesParser`.
  112. """
  113. __slots__ = ("_bnode_ids", "sink", "buffer", "file", "line", "skolemize")
  114. def __init__(
  115. self,
  116. sink: Optional[Union[DummySink, NTGraphSink]] = None,
  117. bnode_context: Optional[_BNodeContextType] = None,
  118. ):
  119. self.skolemize = False
  120. if bnode_context is not None:
  121. self._bnode_ids = bnode_context
  122. else:
  123. self._bnode_ids = {}
  124. self.sink: Union[DummySink, NTGraphSink]
  125. if sink is not None:
  126. self.sink = sink
  127. else:
  128. self.sink = DummySink()
  129. self.buffer: Optional[str] = None
  130. self.file: Optional[Union[TextIO, codecs.StreamReader]] = None
  131. self.line: Optional[str] = ""
  132. def parse(
  133. self,
  134. f: Union[TextIO, IO[bytes], codecs.StreamReader],
  135. bnode_context: Optional[_BNodeContextType] = None,
  136. skolemize: bool = False,
  137. ) -> Union[DummySink, NTGraphSink]:
  138. """Parse f as an N-Triples file.
  139. Args:
  140. f: The N-Triples source
  141. bnode_context: A dict mapping blank node identifiers (e.g., `a` in `_:a`)
  142. to [`BNode`][rdflib.term.BNode] instances. An empty dict can be
  143. passed in to define a distinct context for a given call to
  144. `parse`.
  145. skolemize: Whether to skolemize blank nodes
  146. Returns:
  147. The sink containing the parsed triples
  148. """
  149. if not hasattr(f, "read"):
  150. raise ParseError("Item to parse must be a file-like object.")
  151. if not hasattr(f, "encoding") and not hasattr(f, "charbuffer"):
  152. # someone still using a bytestream here?
  153. f = codecs.getreader("utf-8")(f)
  154. self.skolemize = skolemize
  155. self.file = f # type: ignore[assignment]
  156. self.buffer = ""
  157. while True:
  158. self.line = self.readline()
  159. if self.line is None:
  160. break
  161. try:
  162. self.parseline(bnode_context=bnode_context)
  163. except ParseError:
  164. raise ParseError("Invalid line: {}".format(self.line))
  165. return self.sink
  166. def parsestring(self, s: Union[bytes, bytearray, str], **kwargs) -> None:
  167. """Parse s as an N-Triples string."""
  168. if not isinstance(s, (str, bytes, bytearray)):
  169. raise ParseError("Item to parse must be a string instance.")
  170. f: Union[codecs.StreamReader, StringIO]
  171. if isinstance(s, (bytes, bytearray)):
  172. f = codecs.getreader("utf-8")(BytesIO(s))
  173. else:
  174. f = StringIO(s)
  175. self.parse(f, **kwargs)
  176. def readline(self) -> Optional[str]:
  177. """Read an N-Triples line from buffered input."""
  178. # N-Triples lines end in either CRLF, CR, or LF
  179. # Therefore, we can't just use f.readline()
  180. if not self.buffer:
  181. # type error: Item "None" of "Union[TextIO, StreamReader, None]" has no attribute "read"
  182. buffer = self.file.read(bufsiz) # type: ignore[union-attr]
  183. if not buffer:
  184. return None
  185. self.buffer = buffer
  186. while True:
  187. m = r_line.match(self.buffer)
  188. if m: # the more likely prospect
  189. self.buffer = self.buffer[m.end() :]
  190. return m.group(1)
  191. else:
  192. # type error: Item "None" of "Union[TextIO, StreamReader, None]" has no attribute "read"
  193. buffer = self.file.read(bufsiz) # type: ignore[union-attr]
  194. if not buffer and not self.buffer.isspace():
  195. # Last line does not need to be terminated with a newline
  196. buffer += "\n"
  197. elif not buffer:
  198. return None
  199. self.buffer += buffer
  200. def parseline(self, bnode_context: Optional[_BNodeContextType] = None) -> None:
  201. self.eat(r_wspace)
  202. if (not self.line) or self.line.startswith("#"):
  203. return # The line is empty or a comment
  204. subject = self.subject(bnode_context)
  205. self.eat(r_wspaces)
  206. predicate = self.predicate()
  207. self.eat(r_wspaces)
  208. object_ = self.object(bnode_context)
  209. self.eat(r_tail)
  210. if self.line:
  211. raise ParseError("Trailing garbage: {}".format(self.line))
  212. self.sink.triple(subject, predicate, object_)
  213. def peek(self, token: str) -> bool:
  214. return self.line.startswith(token) # type: ignore[union-attr]
  215. def eat(self, pattern: Pattern[str]) -> Match[str]:
  216. m = pattern.match(self.line) # type: ignore[arg-type]
  217. if not m: # @@ Why can't we get the original pattern?
  218. # print(dir(pattern))
  219. # print repr(self.line), type(self.line)
  220. raise ParseError("Failed to eat %s at %s" % (pattern.pattern, self.line))
  221. self.line = self.line[m.end() :] # type: ignore[index]
  222. return m
  223. def subject(self, bnode_context=None) -> Union[bNode, URIRef]:
  224. # @@ Consider using dictionary cases
  225. subj = self.uriref() or self.nodeid(bnode_context)
  226. if not subj:
  227. raise ParseError("Subject must be uriref or nodeID")
  228. return subj
  229. def predicate(self) -> Union[bNode, URIRef]:
  230. pred = self.uriref()
  231. if not pred:
  232. raise ParseError("Predicate must be uriref")
  233. return pred
  234. def object(
  235. self, bnode_context: Optional[_BNodeContextType] = None
  236. ) -> Union[URI, bNode, Literal]:
  237. objt = self.uriref() or self.nodeid(bnode_context) or self.literal()
  238. if objt is False:
  239. raise ParseError("Unrecognised object type")
  240. return objt
  241. def uriref(self) -> Union[te.Literal[False], URI]:
  242. if self.peek("<"):
  243. uri = self.eat(r_uriref).group(1)
  244. uri = unquote(uri)
  245. uri = uriquote(uri)
  246. return URI(uri)
  247. return False
  248. def nodeid(
  249. self, bnode_context: Optional[_BNodeContextType] = None
  250. ) -> Union[te.Literal[False], bNode, URI]:
  251. if self.peek("_"):
  252. if self.skolemize:
  253. bnode_id = self.eat(r_nodeid).group(1)
  254. return bNode(bnode_id).skolemize()
  255. else:
  256. # Fix for https://github.com/RDFLib/rdflib/issues/204
  257. if bnode_context is None:
  258. bnode_context = self._bnode_ids
  259. bnode_id = self.eat(r_nodeid).group(1)
  260. new_id = bnode_context.get(bnode_id, None)
  261. if new_id is not None:
  262. # Re-map to id specific to this doc
  263. return bNode(new_id)
  264. else:
  265. # Replace with freshly-generated document-specific BNode id
  266. bnode = bNode()
  267. # Store the mapping
  268. bnode_context[bnode_id] = bnode
  269. return bnode
  270. return False
  271. def literal(self) -> Union[te.Literal[False], Literal]:
  272. if self.peek('"'):
  273. lit, lang, dtype = self.eat(r_literal).groups()
  274. if lang:
  275. lang = lang
  276. else:
  277. lang = None
  278. if dtype:
  279. dtype = unquote(dtype)
  280. dtype = uriquote(dtype)
  281. dtype = URI(dtype)
  282. else:
  283. dtype = None
  284. if lang and dtype:
  285. raise ParseError("Can't have both a language and a datatype")
  286. lit = unquote(lit)
  287. return Literal(lit, lang, dtype)
  288. return False
  289. class NTGraphSink:
  290. __slots__ = ("g",)
  291. def __init__(self, graph: Graph):
  292. self.g = graph
  293. def triple(self, s: _SubjectType, p: _PredicateType, o: _ObjectType) -> None:
  294. self.g.add((s, p, o))
  295. class NTParser(Parser):
  296. """Parser for the N-Triples format, often stored with the .nt extension.
  297. See http://www.w3.org/TR/rdf-testcases/#ntriples
  298. """
  299. __slots__ = ()
  300. @classmethod
  301. def parse(cls, source: InputSource, sink: Graph, **kwargs: Any) -> None:
  302. """Parse the NT format.
  303. Args:
  304. source: The source of NT-formatted data
  305. sink: Where to send parsed triples
  306. **kwargs: Additional arguments to pass to `W3CNTriplesParser.parse`
  307. """
  308. f: Union[TextIO, IO[bytes], codecs.StreamReader]
  309. f = source.getCharacterStream()
  310. if not f:
  311. b = source.getByteStream()
  312. # TextIOBase includes: StringIO and TextIOWrapper
  313. if isinstance(b, TextIOBase):
  314. # f is not really a ByteStream, but a CharacterStream
  315. f = b # type: ignore[assignment]
  316. else:
  317. # since N-Triples 1.1 files can and should be utf-8 encoded
  318. f = codecs.getreader("utf-8")(b)
  319. parser = W3CNTriplesParser(NTGraphSink(sink))
  320. parser.parse(f, **kwargs)
  321. f.close()