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.

342 lines
11 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
  1. import csv
  2. import io
  3. import json
  4. import os
  5. from typing import Any, Dict, Iterator, List, Tuple
  6. import chardet
  7. import pyexcel
  8. import pyexcel.exceptions
  9. from chardet import UniversalDetector
  10. from seqeval.scheme import BILOU, IOB2, IOBES, IOE2, Tokens
  11. from .exceptions import FileParseException
  12. from .readers import DEFAULT_LABEL_COLUMN, DEFAULT_TEXT_COLUMN, Parser
  13. DEFAULT_ENCODING = "Auto"
  14. def detect_encoding(filename: str, buffer_size: int = io.DEFAULT_BUFFER_SIZE) -> str:
  15. """Detects character encoding automatically.
  16. If you want to know the supported encodings, please see the following document:
  17. https://chardet.readthedocs.io/en/latest/supported-encodings.html
  18. Args:
  19. filename: the filename for detecting the encoding.
  20. buffer_size: the buffer size to read file contents incrementally.
  21. Returns:
  22. The character encoding.
  23. """
  24. # For a small file.
  25. if os.path.getsize(filename) < buffer_size:
  26. detected = chardet.detect(open(filename, "rb").read())
  27. return detected.get("encoding", "utf-8")
  28. # For a large file, call the Universal Encoding Detector incrementally.
  29. # It will stop as soon as it is confident enough to report its results.
  30. # See: https://chardet.readthedocs.io/en/latest/usage.html
  31. with open(filename, "rb") as f:
  32. detector = UniversalDetector()
  33. while True:
  34. binary = f.read(buffer_size)
  35. detector.feed(binary)
  36. if binary == b"":
  37. break
  38. if detector.done:
  39. break
  40. if detector.done:
  41. return detector.result["encoding"] or "utf-8"
  42. else:
  43. return "utf-8"
  44. def decide_encoding(filename: str, encoding: str) -> str:
  45. """Decide character encoding automatically.
  46. If the encoding is DEFAULT_ENCODING, detects it automatically.
  47. Otherwise, return it as is.
  48. Args:
  49. filename: The filename for decide the encoding.
  50. encoding: The specified encoding.
  51. Returns:
  52. The character encoding.
  53. """
  54. if encoding == DEFAULT_ENCODING:
  55. return detect_encoding(filename)
  56. else:
  57. return encoding
  58. class LineReader:
  59. """LineReader is a helper class to read a file line by line.
  60. Attributes:
  61. filename: The filename to read.
  62. encoding: The character encoding.
  63. """
  64. def __init__(self, filename: str, encoding: str = DEFAULT_ENCODING):
  65. self.filename = filename
  66. self.encoding = encoding
  67. def __iter__(self) -> Iterator[str]:
  68. encoding = decide_encoding(self.filename, self.encoding)
  69. with open(self.filename, encoding=encoding) as f:
  70. for line in f:
  71. yield line.rstrip()
  72. class PlainParser(Parser):
  73. """PlainParser is a parser simply returns a dictionary.
  74. This is for a task without any text.
  75. """
  76. def __init__(self, **kwargs):
  77. self.kwargs = kwargs
  78. def parse(self, filename: str) -> Iterator[Dict[Any, Any]]:
  79. yield {}
  80. class LineParser(Parser):
  81. """LineParser is a parser to read a file line by line.
  82. Attributes:
  83. encoding: The character encoding.
  84. """
  85. def __init__(self, encoding: str = DEFAULT_ENCODING, **kwargs):
  86. self.encoding = encoding
  87. def parse(self, filename: str) -> Iterator[Dict[Any, Any]]:
  88. reader = LineReader(filename, self.encoding)
  89. for line in reader:
  90. yield {DEFAULT_TEXT_COLUMN: line}
  91. class TextFileParser(Parser):
  92. """TextFileParser is a parser to read an entire file content.
  93. Attributes:
  94. encoding: The character encoding.
  95. """
  96. def __init__(self, encoding: str = DEFAULT_ENCODING, **kwargs):
  97. self.encoding = encoding
  98. def parse(self, filename: str) -> Iterator[Dict[Any, Any]]:
  99. encoding = decide_encoding(filename, self.encoding)
  100. with open(filename, encoding=encoding) as f:
  101. yield {DEFAULT_TEXT_COLUMN: f.read()}
  102. class CSVParser(Parser):
  103. """CSVParser is a parser to read a csv file and return its rows.
  104. Attributes:
  105. encoding: The character encoding.
  106. delimiter: A one-character string used to separate fields. It defaults to ','.
  107. """
  108. def __init__(self, encoding: str = DEFAULT_ENCODING, delimiter: str = ",", **kwargs):
  109. self.encoding = encoding
  110. self.delimiter = delimiter
  111. def parse(self, filename: str) -> Iterator[Dict[Any, Any]]:
  112. encoding = decide_encoding(filename, self.encoding)
  113. with open(filename, encoding=encoding) as f:
  114. reader = csv.DictReader(f, delimiter=self.delimiter)
  115. for line_num, row in enumerate(reader, start=2):
  116. yield row
  117. class JSONParser(Parser):
  118. """JSONParser is a parser to read a json file and return its rows.
  119. Attributes:
  120. encoding: The character encoding.
  121. """
  122. def __init__(self, encoding: str = DEFAULT_ENCODING, **kwargs):
  123. self.encoding = encoding
  124. self._errors: List[FileParseException] = []
  125. def parse(self, filename: str) -> Iterator[Dict[Any, Any]]:
  126. encoding = decide_encoding(filename, self.encoding)
  127. with open(filename, encoding=encoding) as f:
  128. try:
  129. rows = json.load(f)
  130. for line_num, row in enumerate(rows, start=1):
  131. yield row
  132. except json.decoder.JSONDecodeError as e:
  133. error = FileParseException(filename, line_num=1, message=str(e))
  134. self._errors.append(error)
  135. @property
  136. def errors(self) -> List[FileParseException]:
  137. return self._errors
  138. class JSONLParser(Parser):
  139. """JSONLParser is a parser to read a JSONL file and return its rows.
  140. Attributes:
  141. encoding: The character encoding.
  142. """
  143. def __init__(self, encoding: str = DEFAULT_ENCODING, **kwargs):
  144. self.encoding = encoding
  145. self._errors: List[FileParseException] = []
  146. def parse(self, filename: str) -> Iterator[Dict[Any, Any]]:
  147. reader = LineReader(filename, self.encoding)
  148. for line_num, line in enumerate(reader, start=1):
  149. try:
  150. yield json.loads(line)
  151. except json.decoder.JSONDecodeError as e:
  152. error = FileParseException(filename, line_num, str(e))
  153. self._errors.append(error)
  154. @property
  155. def errors(self) -> List[FileParseException]:
  156. return self._errors
  157. class ExcelParser(Parser):
  158. """ExcelParser is a parser to read a excel file."""
  159. def __init__(self, **kwargs):
  160. self._errors = []
  161. def parse(self, filename: str) -> Iterator[Dict[Any, Any]]:
  162. rows = pyexcel.iget_records(file_name=filename)
  163. try:
  164. for line_num, row in enumerate(rows, start=1):
  165. yield row
  166. except pyexcel.exceptions.FileTypeNotSupported as e:
  167. error = FileParseException(filename, line_num=1, message=str(e))
  168. self._errors.append(error)
  169. @property
  170. def errors(self) -> List[FileParseException]:
  171. return self._errors
  172. class FastTextParser(Parser):
  173. """FastTextParser is a parser to read a fastText format and returns a text and labels.
  174. The example format is as follows:
  175. __label__positive I really enjoyed this restaurant.
  176. This format expects the category first, with the prefix __label__ before each category,
  177. and then the input text, like so,
  178. Attributes:
  179. encoding: The character encoding.
  180. label: The label prefix. It defaults to `__label__`.
  181. """
  182. def __init__(self, encoding: str = DEFAULT_ENCODING, label: str = "__label__", **kwargs):
  183. self.encoding = encoding
  184. self.label = label
  185. def parse(self, filename: str) -> Iterator[Dict[Any, Any]]:
  186. reader = LineReader(filename, self.encoding)
  187. for line_num, line in enumerate(reader, start=1):
  188. labels = []
  189. tokens = []
  190. for token in line.rstrip().split(" "):
  191. if token.startswith(self.label):
  192. label_name = token[len(self.label) :]
  193. labels.append(label_name)
  194. else:
  195. tokens.append(token)
  196. text = " ".join(tokens)
  197. yield {DEFAULT_TEXT_COLUMN: text, DEFAULT_LABEL_COLUMN: labels}
  198. class CoNLLParser(Parser):
  199. """CoNLLParser is a parser to read conll like format and returns a text and labels.
  200. The example format is as follows:
  201. EU B-ORG
  202. rejects O
  203. German B-MISC
  204. call O
  205. to O
  206. boycott O
  207. British B-MISC
  208. lamb O
  209. . O
  210. Peter B-PER
  211. Blackburn I-PER
  212. This format expects a token in the first column, and a tag in the second column.
  213. The each data is separated by a new line.
  214. Attributes:
  215. encoding: The character encoding.
  216. delimiter: A one-character string used to separate fields. It defaults to ' '.
  217. scheme: The tagging scheme. It supports `IOB2`, `IOE2`, `IOBES`, and `BILOU`.
  218. """
  219. def __init__(self, encoding: str = DEFAULT_ENCODING, delimiter: str = " ", scheme: str = "IOB2", **kwargs):
  220. self.encoding = encoding
  221. self.delimiter = delimiter
  222. mapping = {"IOB2": IOB2, "IOE2": IOE2, "IOBES": IOBES, "BILOU": BILOU}
  223. self._errors: List[FileParseException] = []
  224. if scheme in mapping:
  225. self.scheme = mapping[scheme]
  226. else:
  227. self.scheme = None
  228. @property
  229. def errors(self) -> List[FileParseException]:
  230. return self._errors
  231. def parse(self, filename: str) -> Iterator[Dict[Any, Any]]:
  232. if not self.scheme:
  233. message = "The specified scheme is not supported."
  234. error = FileParseException(filename, line_num=1, message=message)
  235. self._errors.append(error)
  236. return
  237. reader = LineReader(filename, self.encoding)
  238. words, tags = [], []
  239. for line_num, line in enumerate(reader, start=1):
  240. line = line.rstrip()
  241. if line:
  242. tokens = line.split("\t")
  243. if len(tokens) != 2:
  244. message = "A line must be separated by tab and has two columns."
  245. self._errors.append(FileParseException(filename, line_num, message))
  246. return
  247. word, tag = tokens
  248. words.append(word)
  249. tags.append(tag)
  250. else:
  251. yield self.create_record(tags, words)
  252. words, tags = [], []
  253. if words:
  254. yield self.create_record(tags, words)
  255. def create_record(self, tags, words):
  256. text = self.delimiter.join(words)
  257. labels = self.align_span(words, tags)
  258. return {DEFAULT_TEXT_COLUMN: text, DEFAULT_LABEL_COLUMN: labels}
  259. def align_span(self, words: List[str], tags: List[str]) -> List[Tuple[int, int, str]]:
  260. tokens = Tokens(tags, self.scheme)
  261. labels = []
  262. for entity in tokens.entities:
  263. text = self.delimiter.join(words[: entity.start])
  264. start = len(text) + len(self.delimiter) if text else len(text)
  265. chunk = words[entity.start : entity.end]
  266. text = self.delimiter.join(chunk)
  267. end = start + len(text)
  268. labels.append((start, end, entity.tag))
  269. return labels