chemdataextractor-lite 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. chemdataextractor/__init__.py +13 -0
  2. chemdataextractor/data/README.md +3 -0
  3. chemdataextractor/data/punkt_chem-1.0.pickle +0 -0
  4. chemdataextractor/doc/__init__.py +17 -0
  5. chemdataextractor/doc/document.py +168 -0
  6. chemdataextractor/doc/element.py +59 -0
  7. chemdataextractor/doc/figure.py +14 -0
  8. chemdataextractor/doc/meta.py +30 -0
  9. chemdataextractor/doc/table.py +36 -0
  10. chemdataextractor/doc/text.py +224 -0
  11. chemdataextractor/errors.py +9 -0
  12. chemdataextractor/nlp/__init__.py +8 -0
  13. chemdataextractor/nlp/tokenize.py +1021 -0
  14. chemdataextractor/reader/__init__.py +46 -0
  15. chemdataextractor/reader/_helpers.py +67 -0
  16. chemdataextractor/reader/acs.py +42 -0
  17. chemdataextractor/reader/base.py +40 -0
  18. chemdataextractor/reader/cssp.py +45 -0
  19. chemdataextractor/reader/elsevier.py +247 -0
  20. chemdataextractor/reader/markup.py +359 -0
  21. chemdataextractor/reader/nlm.py +63 -0
  22. chemdataextractor/reader/pdf.py +145 -0
  23. chemdataextractor/reader/plaintext.py +35 -0
  24. chemdataextractor/reader/rsc.py +93 -0
  25. chemdataextractor/reader/springer.py +122 -0
  26. chemdataextractor/reader/springer_jats.py +88 -0
  27. chemdataextractor/reader/uspto.py +159 -0
  28. chemdataextractor/scrape/__init__.py +15 -0
  29. chemdataextractor/scrape/clean.py +218 -0
  30. chemdataextractor/scrape/csstranslator.py +120 -0
  31. chemdataextractor/text/__init__.py +375 -0
  32. chemdataextractor_lite-1.0.0.dist-info/METADATA +72 -0
  33. chemdataextractor_lite-1.0.0.dist-info/RECORD +36 -0
  34. chemdataextractor_lite-1.0.0.dist-info/WHEEL +5 -0
  35. chemdataextractor_lite-1.0.0.dist-info/licenses/LICENSE +50 -0
  36. chemdataextractor_lite-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,13 @@
1
+ """Lightweight scientific document loading."""
2
+
3
+ import logging
4
+
5
+ from .doc.document import Document
6
+
7
+ __title__ = "ChemDataExtractor Lite"
8
+ __version__ = "1.0.0"
9
+ __license__ = "MIT"
10
+
11
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
12
+
13
+ __all__ = ["Document"]
@@ -0,0 +1,3 @@
1
+ # Bundled sentence model
2
+
3
+ `punkt_chem-1.0.pickle` is the chemistry-trained Punkt sentence tokenizer distributed by the ChemDataExtractor project. It is bundled here so sentence segmentation does not perform network access at runtime.
@@ -0,0 +1,17 @@
1
+ """Structural document model."""
2
+
3
+ from .document import Document
4
+ from .element import BaseElement, CaptionedElement
5
+ from .figure import Figure
6
+ from .meta import MetaData
7
+ from .table import Table
8
+ from .text import (
9
+ Caption, Cell, Citation, Footnote, Heading, Paragraph, RichToken, Sentence,
10
+ Span, Text, Title, Token,
11
+ )
12
+
13
+ __all__ = [
14
+ "BaseElement", "Caption", "CaptionedElement", "Cell", "Citation", "Document",
15
+ "Figure", "Footnote", "Heading", "MetaData", "Paragraph", "RichToken",
16
+ "Sentence", "Span", "Table", "Text", "Title", "Token",
17
+ ]
@@ -0,0 +1,168 @@
1
+ """Document container and reader dispatch."""
2
+
3
+ import collections
4
+ import io
5
+ import json
6
+ import logging
7
+ from pathlib import Path
8
+
9
+ from ..errors import ReaderError
10
+ from ..text import get_encoding
11
+ from .element import CaptionedElement
12
+ from .figure import Figure
13
+ from .meta import MetaData
14
+ from .table import Table
15
+ from .text import Caption, Cell, Citation, Footnote, Heading, Paragraph, Sentence, Title
16
+
17
+ log = logging.getLogger(__name__)
18
+
19
+
20
+ class Document(collections.abc.Sequence):
21
+ """A format-neutral sequence of structural document elements."""
22
+
23
+ def __init__(self, *elements):
24
+ self._elements = []
25
+ for element in elements:
26
+ if isinstance(element, str):
27
+ element = Paragraph(element)
28
+ elif isinstance(element, bytes):
29
+ encoding = get_encoding(element)
30
+ log.warning("Guessed bytestring encoding as %s", encoding)
31
+ element = Paragraph(element.decode(encoding))
32
+ element.document = self
33
+ self._elements.append(element)
34
+
35
+ def __repr__(self):
36
+ return f"<Document: {len(self)} elements>"
37
+
38
+ __str__ = __repr__
39
+
40
+ def __getitem__(self, index):
41
+ return self.elements[index]
42
+
43
+ def __len__(self):
44
+ return len(self.elements)
45
+
46
+ @property
47
+ def elements(self):
48
+ return self._elements
49
+
50
+ @classmethod
51
+ def from_file(cls, file, fname=None, readers=None):
52
+ if isinstance(file, (str, bytes, Path)):
53
+ path = Path(file)
54
+ with path.open("rb") as stream:
55
+ return cls.from_string(stream.read(), fname=fname or str(path), readers=readers)
56
+ if fname is None and hasattr(file, "name"):
57
+ fname = file.name
58
+ return cls.from_string(file.read(), fname=fname, readers=readers)
59
+
60
+ @classmethod
61
+ def from_string(cls, content, fname=None, readers=None):
62
+ if isinstance(content, str):
63
+ content = content.encode("utf-8")
64
+ if not isinstance(content, bytes):
65
+ raise TypeError("content must be bytes or str")
66
+ if readers is None:
67
+ from ..reader import DEFAULT_READERS
68
+ readers = DEFAULT_READERS
69
+ errors = []
70
+ for reader in readers:
71
+ try:
72
+ if not reader.detect(content, fname=fname):
73
+ continue
74
+ document = reader.readstring(content)
75
+ log.debug("Parsed document with %s", reader.__class__.__name__)
76
+ return document
77
+ except ReaderError as error:
78
+ errors.append(error)
79
+ message = "Unable to read document"
80
+ if errors:
81
+ message += f": {errors[-1]}"
82
+ raise ReaderError(message)
83
+
84
+ def get_element_with_id(self, id):
85
+ pending = list(self.elements)
86
+ while pending:
87
+ element = pending.pop(0)
88
+ if element.id == id:
89
+ return element
90
+ if element.elements:
91
+ pending.extend(element.elements)
92
+ return None
93
+
94
+ def _elements_of_type(self, element_type):
95
+ return [element for element in self.elements if isinstance(element, element_type)]
96
+
97
+ @property
98
+ def figures(self):
99
+ return self._elements_of_type(Figure)
100
+
101
+ @property
102
+ def tables(self):
103
+ return self._elements_of_type(Table)
104
+
105
+ @property
106
+ def citations(self):
107
+ return self._elements_of_type(Citation)
108
+
109
+ @property
110
+ def footnotes(self):
111
+ return self._elements_of_type(Footnote)
112
+
113
+ @property
114
+ def titles(self):
115
+ return self._elements_of_type(Title)
116
+
117
+ @property
118
+ def headings(self):
119
+ return self._elements_of_type(Heading)
120
+
121
+ @property
122
+ def paragraphs(self):
123
+ return self._elements_of_type(Paragraph)
124
+
125
+ @property
126
+ def captions(self):
127
+ return self._elements_of_type(Caption)
128
+
129
+ @property
130
+ def captioned_elements(self):
131
+ return self._elements_of_type(CaptionedElement)
132
+
133
+ @property
134
+ def metadata(self):
135
+ metadata = self._elements_of_type(MetaData)
136
+ return metadata[0] if metadata else None
137
+
138
+ @property
139
+ def sentences(self):
140
+ pending = list(self.elements)
141
+ sentences = []
142
+ while pending:
143
+ element = pending.pop(0)
144
+ if isinstance(element, Sentence) and not isinstance(element, Cell):
145
+ sentences.append(element)
146
+ elif element.elements:
147
+ pending[0:0] = element.elements
148
+ return sentences
149
+
150
+ def heading_for_sentence(self, sentence):
151
+ current_heading = None
152
+ for element in self.elements:
153
+ if isinstance(element, Heading):
154
+ current_heading = element
155
+ continue
156
+ if sentence in (element.elements or []):
157
+ return current_heading
158
+ return None
159
+
160
+ def serialize(self):
161
+ return {"type": "document", "elements": [element.serialize() for element in self.elements]}
162
+
163
+ def to_json(self, *args, **kwargs):
164
+ return json.dumps(self.serialize(), *args, **kwargs)
165
+
166
+ def _repr_html_(self):
167
+ body = "\n".join(element._repr_html_() for element in self.elements if hasattr(element, "_repr_html_"))
168
+ return f'<div class="cde-document">\n{body}\n</div>'
@@ -0,0 +1,59 @@
1
+ """Base classes for document elements."""
2
+
3
+ import json
4
+ from abc import ABCMeta, abstractmethod
5
+
6
+
7
+ class BaseElement(metaclass=ABCMeta):
8
+ def __init__(self, document=None, references=None, id=None, **kwargs):
9
+ self._document = document
10
+ self.id = id
11
+ self.references = list(references or [])
12
+
13
+ def __repr__(self):
14
+ return f"<{self.__class__.__name__}>"
15
+
16
+ @property
17
+ def document(self):
18
+ return self._document
19
+
20
+ @document.setter
21
+ def document(self, document):
22
+ self._document = document
23
+
24
+ @property
25
+ def elements(self):
26
+ return None
27
+
28
+ @abstractmethod
29
+ def serialize(self):
30
+ raise NotImplementedError
31
+
32
+ def to_json(self, *args, **kwargs):
33
+ return json.dumps(self.serialize(), *args, **kwargs)
34
+
35
+
36
+ class CaptionedElement(BaseElement):
37
+ def __init__(self, caption, label=None, **kwargs):
38
+ self.caption = caption
39
+ self.label = label
40
+ super().__init__(**kwargs)
41
+ self.caption.document = self.document
42
+
43
+ def __str__(self):
44
+ return self.caption.text
45
+
46
+ @BaseElement.document.setter
47
+ def document(self, document):
48
+ self._document = document
49
+ self.caption.document = document
50
+
51
+ @property
52
+ def elements(self):
53
+ return [self.caption]
54
+
55
+ def serialize(self):
56
+ data = {"type": self.__class__.__name__, "caption": self.caption.serialize()}
57
+ if self.label is not None:
58
+ data["label"] = self.label
59
+ return data
@@ -0,0 +1,14 @@
1
+ """Figure document element."""
2
+
3
+ from .element import CaptionedElement
4
+
5
+
6
+ class Figure(CaptionedElement):
7
+ def __init__(self, caption, label=None, links=None, **kwargs):
8
+ super().__init__(caption=caption, label=label, **kwargs)
9
+ self.links = list(links or [])
10
+
11
+ def serialize(self):
12
+ data = super().serialize()
13
+ data["links"] = self.links
14
+ return data
@@ -0,0 +1,30 @@
1
+ """Bibliographic metadata document element."""
2
+
3
+ from .element import BaseElement
4
+
5
+
6
+ class MetaData(BaseElement):
7
+ FIELDS = (
8
+ "title", "authors", "publisher", "journal", "volume", "issue",
9
+ "firstpage", "lastpage", "doi", "date", "language", "pdf_url", "html_url",
10
+ )
11
+
12
+ def __init__(self, data, **kwargs):
13
+ super().__init__(**kwargs)
14
+ normalized = {key.lstrip("_"): value for key, value in data.items()}
15
+ self._data = {key: normalized.get(key) for key in self.FIELDS}
16
+
17
+ def __repr__(self):
18
+ return repr({key: value for key, value in self.data.items() if value})
19
+
20
+ def __getattr__(self, name):
21
+ if name in self.FIELDS:
22
+ return self._data[name]
23
+ raise AttributeError(name)
24
+
25
+ @property
26
+ def data(self):
27
+ return dict(self._data)
28
+
29
+ def serialize(self):
30
+ return {"MetaData": {key: value for key, value in self._data.items() if value}}
@@ -0,0 +1,36 @@
1
+ """Structural table document element."""
2
+
3
+ from .element import CaptionedElement
4
+ from .text import Cell
5
+
6
+
7
+ class Table(CaptionedElement):
8
+ def __init__(self, caption, label=None, table_data=None, footnotes=None, **kwargs):
9
+ super().__init__(caption=caption, label=label, **kwargs)
10
+ self.table_data = [
11
+ [cell if isinstance(cell, Cell) else Cell(str(cell)) for cell in row]
12
+ for row in (table_data or [])
13
+ ]
14
+ self.footnotes = list(footnotes or [])
15
+ self.document = self.document
16
+
17
+ @CaptionedElement.document.setter
18
+ def document(self, document):
19
+ self._document = document
20
+ self.caption.document = document
21
+ for row in getattr(self, "table_data", []):
22
+ for cell in row:
23
+ cell.document = document
24
+ for footnote in getattr(self, "footnotes", []):
25
+ footnote.document = document
26
+
27
+ @property
28
+ def elements(self):
29
+ return [cell for row in self.table_data for cell in row] + [self.caption] + self.footnotes
30
+
31
+ def serialize(self):
32
+ data = super().serialize()
33
+ data["rows"] = [[cell.text for cell in row] for row in self.table_data]
34
+ if self.footnotes:
35
+ data["footnotes"] = [footnote.serialize() for footnote in self.footnotes]
36
+ return data
@@ -0,0 +1,224 @@
1
+ """Text document elements with sentence and word segmentation."""
2
+
3
+ import collections
4
+ import unicodedata
5
+
6
+ from ..nlp.tokenize import ChemSentenceTokenizer, ChemWordTokenizer
7
+ from .element import BaseElement
8
+
9
+
10
+ class Span:
11
+ def __init__(self, text, start, end):
12
+ self.text = text
13
+ self.start = start
14
+ self.end = end
15
+
16
+ def __repr__(self):
17
+ return f"{self.__class__.__name__}({self.text!r}, {self.start!r}, {self.end!r})"
18
+
19
+ def __str__(self):
20
+ return self.text
21
+
22
+ def __eq__(self, other):
23
+ return (
24
+ isinstance(other, self.__class__)
25
+ and self.text == other.text
26
+ and self.start == other.start
27
+ and self.end == other.end
28
+ )
29
+
30
+ def __hash__(self):
31
+ return hash((self.text, self.start, self.end))
32
+
33
+ @property
34
+ def length(self):
35
+ return self.end - self.start
36
+
37
+
38
+ class Token(Span):
39
+ pass
40
+
41
+
42
+ # Kept as an import alias for loader clients that used the old token class name.
43
+ RichToken = Token
44
+
45
+
46
+ class BaseText(BaseElement):
47
+ def __init__(self, text, **kwargs):
48
+ if not isinstance(text, str):
49
+ raise TypeError("Text must be a unicode string")
50
+ super().__init__(**kwargs)
51
+ self._text = text
52
+
53
+ def __repr__(self):
54
+ return (
55
+ f"{self.__class__.__name__}(id={self.id!r}, "
56
+ f"references={self.references!r}, text={self.text!r})"
57
+ )
58
+
59
+ def __str__(self):
60
+ return self.text
61
+
62
+ @property
63
+ def text(self):
64
+ return self._text
65
+
66
+ def serialize(self):
67
+ data = {"type": self.__class__.__name__, "content": self.text}
68
+ if self.id is not None:
69
+ data["id"] = self.id
70
+ if self.references:
71
+ data["references"] = self.references
72
+ return data
73
+
74
+ def _repr_html_(self):
75
+ return self.text
76
+
77
+
78
+ class Text(collections.abc.Sequence, BaseText):
79
+ sentence_tokenizer = ChemSentenceTokenizer()
80
+ word_tokenizer = ChemWordTokenizer()
81
+
82
+ def __init__(self, text, sentence_tokenizer=None, word_tokenizer=None, **kwargs):
83
+ super().__init__(text, **kwargs)
84
+ self.sentence_tokenizer = sentence_tokenizer or self.sentence_tokenizer
85
+ self.word_tokenizer = word_tokenizer or self.word_tokenizer
86
+ self._sentences = None
87
+
88
+ def __getitem__(self, index):
89
+ return self.sentences[index]
90
+
91
+ def __len__(self):
92
+ return len(self.sentences)
93
+
94
+ @property
95
+ def sentences(self):
96
+ if self._sentences is None:
97
+ spans = self.sentence_tokenizer.span_tokenize(self.text)
98
+ self._sentences = [
99
+ Sentence(
100
+ self.text[start:end],
101
+ start=start,
102
+ end=end,
103
+ word_tokenizer=self.word_tokenizer,
104
+ document=self.document,
105
+ )
106
+ for start, end in spans
107
+ ]
108
+ return self._sentences
109
+
110
+ @BaseElement.document.setter
111
+ def document(self, document):
112
+ self._document = document
113
+ if self._sentences is not None:
114
+ for sentence in self._sentences:
115
+ sentence.document = document
116
+
117
+ @property
118
+ def elements(self):
119
+ return self.sentences
120
+
121
+ @property
122
+ def raw_sentences(self):
123
+ return [sentence.text for sentence in self.sentences]
124
+
125
+ @property
126
+ def tokens(self):
127
+ return [sentence.tokens for sentence in self.sentences]
128
+
129
+ @property
130
+ def raw_tokens(self):
131
+ return [sentence.raw_tokens for sentence in self.sentences]
132
+
133
+ def __add__(self, other):
134
+ if type(self) is type(other):
135
+ return self.__class__(
136
+ self.text + other.text,
137
+ id=self.id or other.id,
138
+ references=self.references + other.references,
139
+ sentence_tokenizer=self.sentence_tokenizer,
140
+ word_tokenizer=self.word_tokenizer,
141
+ )
142
+ return NotImplemented
143
+
144
+
145
+ class Title(Text):
146
+ def _repr_html_(self):
147
+ return f'<h1 class="cde-title">{self.text}</h1>'
148
+
149
+
150
+ class Heading(Text):
151
+ def _repr_html_(self):
152
+ return f'<h2 class="cde-heading">{self.text}</h2>'
153
+
154
+
155
+ class Paragraph(Text):
156
+ def _repr_html_(self):
157
+ return f'<p class="cde-paragraph">{self.text}</p>'
158
+
159
+
160
+ class Footnote(Text):
161
+ pass
162
+
163
+
164
+ class Citation(Text):
165
+ pass
166
+
167
+
168
+ class Caption(Text):
169
+ pass
170
+
171
+
172
+ class Sentence(collections.abc.Sequence, BaseText):
173
+ word_tokenizer = ChemWordTokenizer()
174
+
175
+ def __init__(self, text, start=0, end=None, word_tokenizer=None, **kwargs):
176
+ super().__init__(text, **kwargs)
177
+ self.start = start
178
+ self.end = len(text) if end is None else end
179
+ self.word_tokenizer = word_tokenizer or self.word_tokenizer
180
+ self._tokens = None
181
+
182
+ def __getitem__(self, index):
183
+ return self.tokens[index]
184
+
185
+ def __len__(self):
186
+ return len(self.tokens)
187
+
188
+ @property
189
+ def tokens(self):
190
+ if self._tokens is None:
191
+ spans = self.word_tokenizer.span_tokenize(self.text)
192
+ self._tokens = [
193
+ Token(
194
+ "".join(ch for ch in self.text[left:right] if unicodedata.category(ch)[0] != "C"),
195
+ left + self.start,
196
+ right + self.start,
197
+ )
198
+ for left, right in spans
199
+ ]
200
+ return self._tokens
201
+
202
+ @property
203
+ def raw_tokens(self):
204
+ return [token.text for token in self.tokens]
205
+
206
+ @property
207
+ def elements(self):
208
+ return None
209
+
210
+ def __add__(self, other):
211
+ if type(self) is type(other):
212
+ return self.__class__(
213
+ self.text + other.text,
214
+ start=self.start,
215
+ end=self.start + len(self.text + other.text),
216
+ id=self.id or other.id,
217
+ references=self.references + other.references,
218
+ word_tokenizer=self.word_tokenizer,
219
+ )
220
+ return NotImplemented
221
+
222
+
223
+ class Cell(Sentence):
224
+ pass
@@ -0,0 +1,9 @@
1
+ """Public exceptions raised by document loaders."""
2
+
3
+
4
+ class ChemDataExtractorError(Exception):
5
+ """Base package exception."""
6
+
7
+
8
+ class ReaderError(ChemDataExtractorError):
9
+ """Raised when a reader cannot load a document."""
@@ -0,0 +1,8 @@
1
+ """Sentence and word segmentation utilities."""
2
+
3
+ from .tokenize import ChemSentenceTokenizer, ChemWordTokenizer, FineWordTokenizer, SentenceTokenizer, WordTokenizer
4
+
5
+ __all__ = [
6
+ "ChemSentenceTokenizer", "ChemWordTokenizer", "FineWordTokenizer",
7
+ "SentenceTokenizer", "WordTokenizer",
8
+ ]