chunk_serializer.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. """
  2. This file provides a single function `serialize_in_chunks()` which can serialize a
  3. Graph into a number of NT files with a maximum number of triples or maximum file size.
  4. There is an option to preserve any prefixes declared for the original graph in the first
  5. file, which will be a Turtle file.
  6. """
  7. from __future__ import annotations
  8. from contextlib import ExitStack, contextmanager
  9. from pathlib import Path
  10. from typing import TYPE_CHECKING, BinaryIO, Generator, Optional, Tuple
  11. from rdflib.graph import Graph
  12. from rdflib.plugins.serializers.nt import _nt_row
  13. # from rdflib.term import Literal
  14. # if TYPE_CHECKING:
  15. # from rdflib.graph import _TriplePatternType
  16. __all__ = ["serialize_in_chunks"]
  17. def serialize_in_chunks(
  18. g: Graph,
  19. max_triples: int = 10000,
  20. max_file_size_kb: Optional[int] = None,
  21. file_name_stem: str = "chunk",
  22. output_dir: Optional[Path] = None,
  23. write_prefixes: bool = False,
  24. ) -> None:
  25. """Serializes a given Graph into a series of n-triples with a given length.
  26. Args:
  27. g: The graph to serialize.
  28. max_file_size_kb: Maximum size per NT file in kB (1,000 bytes)
  29. Equivalent to ~6,000 triples, depending on Literal sizes.
  30. max_triples: Maximum size per NT file in triples
  31. Equivalent to lines in file.
  32. If both this parameter and max_file_size_kb are set, max_file_size_kb will be used.
  33. file_name_stem: Prefix of each file name.
  34. e.g. "chunk" = chunk_000001.nt, chunk_000002.nt...
  35. output_dir: The directory you want the files to be written to.
  36. write_prefixes: The first file created is a Turtle file containing original graph prefixes.
  37. See `../test/test_tools/test_chunk_serializer.py` for examples of this in use.
  38. """
  39. if output_dir is None:
  40. output_dir = Path.cwd()
  41. if not output_dir.is_dir():
  42. raise ValueError(
  43. "If you specify an output_dir, it must actually be a directory!"
  44. )
  45. @contextmanager
  46. def _start_new_file(file_no: int) -> Generator[Tuple[Path, BinaryIO], None, None]:
  47. if TYPE_CHECKING:
  48. # this is here because mypy gets a bit confused
  49. assert output_dir is not None
  50. fp = Path(output_dir) / f"{file_name_stem}_{str(file_no).zfill(6)}.nt"
  51. with open(fp, "wb") as fh:
  52. yield fp, fh
  53. def _serialize_prefixes(g: Graph) -> str:
  54. pres = []
  55. for k, v in g.namespace_manager.namespaces():
  56. pres.append(f"PREFIX {k}: <{v}>")
  57. return "\n".join(sorted(pres)) + "\n"
  58. if write_prefixes:
  59. with open(
  60. Path(output_dir) / f"{file_name_stem}_000000.ttl", "w", encoding="utf-8"
  61. ) as fh:
  62. fh.write(_serialize_prefixes(g))
  63. bytes_written = 0
  64. with ExitStack() as xstack:
  65. if max_file_size_kb is not None:
  66. max_file_size = max_file_size_kb * 1000
  67. file_no = 1 if write_prefixes else 0
  68. for i, t in enumerate(g.triples((None, None, None))):
  69. row_bytes = _nt_row(t).encode("utf-8")
  70. if len(row_bytes) > max_file_size:
  71. raise ValueError(
  72. # type error: Unsupported operand types for / ("bytes" and "int")
  73. f"cannot write triple {t!r} as it's serialized size of {row_bytes / 1000} exceeds max_file_size_kb = {max_file_size_kb}" # type: ignore[operator]
  74. )
  75. if i == 0:
  76. fp, fhb = xstack.enter_context(_start_new_file(file_no))
  77. bytes_written = 0
  78. elif (bytes_written + len(row_bytes)) >= max_file_size:
  79. file_no += 1
  80. fp, fhb = xstack.enter_context(_start_new_file(file_no))
  81. bytes_written = 0
  82. bytes_written += fhb.write(row_bytes)
  83. else:
  84. # count the triples in the graph
  85. graph_length = len(g)
  86. if graph_length <= max_triples:
  87. # the graph is less than max so just NT serialize the whole thing
  88. g.serialize(
  89. destination=Path(output_dir) / f"{file_name_stem}_all.nt",
  90. format="nt",
  91. )
  92. else:
  93. # graph_length is > max_lines, make enough files for all graph
  94. # no_files = math.ceil(graph_length / max_triples)
  95. file_no = 1 if write_prefixes else 0
  96. for i, t in enumerate(g.triples((None, None, None))):
  97. if i % max_triples == 0:
  98. fp, fhb = xstack.enter_context(_start_new_file(file_no))
  99. file_no += 1
  100. fhb.write(_nt_row(t).encode("utf-8"))
  101. return