textdirectory 0.4.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.
- textdirectory/__init__.py +11 -0
- textdirectory/__main__.py +6 -0
- textdirectory/cli.py +99 -0
- textdirectory/crudespellchecker.py +179 -0
- textdirectory/data/language_models/crudesc_lm_ame.gz.lm +0 -0
- textdirectory/data/language_models/crudesc_lm_amehistorical.gz.lm +0 -0
- textdirectory/data/language_models/crudesc_lm_en.gz.lm +0 -0
- textdirectory/data/stopwords/stopwords_en.txt +96 -0
- textdirectory/helpers.py +273 -0
- textdirectory/py.typed +0 -0
- textdirectory/textdirectory.py +607 -0
- textdirectory/transformations.py +440 -0
- textdirectory-0.4.0.dist-info/METADATA +262 -0
- textdirectory-0.4.0.dist-info/RECORD +17 -0
- textdirectory-0.4.0.dist-info/WHEEL +4 -0
- textdirectory-0.4.0.dist-info/entry_points.txt +2 -0
- textdirectory-0.4.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Top-level package for textdirectory."""
|
|
2
|
+
|
|
3
|
+
__author__ = 'Ingo Kleiber'
|
|
4
|
+
__email__ = 'ingo@kleiber.me'
|
|
5
|
+
__version__ = '0.4.0'
|
|
6
|
+
|
|
7
|
+
from textdirectory import helpers, transformations
|
|
8
|
+
from textdirectory.crudespellchecker import CrudeSpellChecker
|
|
9
|
+
from textdirectory.textdirectory import TextDirectory
|
|
10
|
+
|
|
11
|
+
__all__ = ['CrudeSpellChecker', 'TextDirectory', '__version__', 'helpers', 'transformations']
|
textdirectory/cli.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Console script for textdirectory."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from textdirectory import helpers, textdirectory
|
|
9
|
+
|
|
10
|
+
available_filters = helpers.get_available_filters()
|
|
11
|
+
available_transformations = helpers.get_available_transformations()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command()
|
|
15
|
+
@click.version_option(package_name='textdirectory')
|
|
16
|
+
@click.option('--directory', help='The directory containing text files', type=str)
|
|
17
|
+
@click.option('--output_file', help='The file to aggregate to', type=str)
|
|
18
|
+
@click.option('--filetype', help='The file type to look for.', default='txt', type=str)
|
|
19
|
+
@click.option('--encoding', help='The encoding of the files.', default='utf8', type=str)
|
|
20
|
+
@click.option('--recursive', help='Recursion', type=bool)
|
|
21
|
+
@click.option('--disable_tqdm', help='Disable progress bar', default=False, type=bool)
|
|
22
|
+
@click.option('--filters', help=f'The filters you want to apply. Filters: {available_filters}', type=str)
|
|
23
|
+
@click.option(
|
|
24
|
+
'--transformations',
|
|
25
|
+
help=f'The transformations you want to apply. Tranformations: {available_transformations}',
|
|
26
|
+
type=str,
|
|
27
|
+
)
|
|
28
|
+
def main(
|
|
29
|
+
directory: str | None,
|
|
30
|
+
output_file: str | None,
|
|
31
|
+
filetype: str,
|
|
32
|
+
encoding: str,
|
|
33
|
+
recursive: bool,
|
|
34
|
+
disable_tqdm: bool,
|
|
35
|
+
filters: str | None,
|
|
36
|
+
transformations: str | None,
|
|
37
|
+
) -> int:
|
|
38
|
+
"""Console script for textdirectory."""
|
|
39
|
+
if not directory:
|
|
40
|
+
click.echo('Welcome to TextDirectory!\nRun textdirectory --help for further information.')
|
|
41
|
+
click.echo(
|
|
42
|
+
'Example (Basic Aggregation): textdirectory --directory testdata --output_file aggregated.txt --filetype txt'
|
|
43
|
+
)
|
|
44
|
+
sys.exit()
|
|
45
|
+
|
|
46
|
+
if filters:
|
|
47
|
+
filters_list: list[list[Any]] = []
|
|
48
|
+
for filter in filters.split('/'):
|
|
49
|
+
name, *filter_args = filter.split(',')
|
|
50
|
+
|
|
51
|
+
# Coerce string arguments (e.g. '100') to the types the filter expects
|
|
52
|
+
filter_method = getattr(textdirectory.TextDirectory, name, None)
|
|
53
|
+
if filter_method is not None and name in available_filters:
|
|
54
|
+
filter_args = helpers.coerce_args_by_signature(filter_method, filter_args)
|
|
55
|
+
|
|
56
|
+
filters_list.append([name, *filter_args])
|
|
57
|
+
|
|
58
|
+
if transformations:
|
|
59
|
+
transformations_list: list[list[str]] = []
|
|
60
|
+
for transformation in transformations.split('/'):
|
|
61
|
+
transformations_list.append(transformation.split(','))
|
|
62
|
+
|
|
63
|
+
if disable_tqdm or not output_file:
|
|
64
|
+
disable_tqdm = True
|
|
65
|
+
else:
|
|
66
|
+
disable_tqdm = False
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
td = textdirectory.TextDirectory(directory=directory, encoding=encoding, disable_tqdm=disable_tqdm)
|
|
70
|
+
td.load_files(recursive=recursive, filetype=filetype)
|
|
71
|
+
except NotADirectoryError:
|
|
72
|
+
click.echo('The directory could not be found.')
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
except FileNotFoundError:
|
|
75
|
+
click.echo('There seem to be no files. Maybe you want to run with --recursive True.')
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
if filters and len(filters_list) > 0:
|
|
80
|
+
td.run_filters(filters_list)
|
|
81
|
+
|
|
82
|
+
if transformations and len(transformations_list) > 0:
|
|
83
|
+
for staged_transformation in transformations_list:
|
|
84
|
+
td.stage_transformation(staged_transformation)
|
|
85
|
+
except NameError as e:
|
|
86
|
+
click.echo(str(e))
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
|
|
89
|
+
if output_file:
|
|
90
|
+
td.print_aggregation()
|
|
91
|
+
td.aggregate_to_file(output_file)
|
|
92
|
+
else:
|
|
93
|
+
click.echo(td.aggregate_to_memory())
|
|
94
|
+
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == '__main__':
|
|
99
|
+
sys.exit(main())
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Spellchecker module."""
|
|
2
|
+
|
|
3
|
+
import gzip
|
|
4
|
+
import importlib.resources
|
|
5
|
+
import pickle
|
|
6
|
+
import re
|
|
7
|
+
from collections import Counter
|
|
8
|
+
from collections.abc import Iterable, Iterator
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CrudeSpellChecker:
|
|
14
|
+
"""A very simple and crude spellchecker based on Peter Norvig's design.
|
|
15
|
+
Simple Language Models:
|
|
16
|
+
crudesc_lm_en.gz.lm English based on COCA (sample), OANC (written), BNC
|
|
17
|
+
crudesc_lm_ame.lm American English based on COCA (sample) and OANC (written)
|
|
18
|
+
crudesc_lm_amehistorical.lm American English based on COHA (sample)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, caching: bool = True, language_model: str = 'crudesc_lm_en') -> None:
|
|
22
|
+
"""
|
|
23
|
+
:param caching: caching of corrections
|
|
24
|
+
:type caching: bool
|
|
25
|
+
:param language_model: the name of the lm
|
|
26
|
+
:type language_model: str
|
|
27
|
+
"""
|
|
28
|
+
self.caching = caching
|
|
29
|
+
self.cache: dict[str, str] = {}
|
|
30
|
+
self.language_model_name = language_model
|
|
31
|
+
|
|
32
|
+
model_resource = importlib.resources.files('textdirectory').joinpath( # type: ignore[call-arg]
|
|
33
|
+
'data', 'language_models', f'{self.language_model_name}.gz.lm'
|
|
34
|
+
)
|
|
35
|
+
self.frequencies: Counter[str] = pickle.loads(gzip.decompress(model_resource.read_bytes()))
|
|
36
|
+
|
|
37
|
+
def p_word(self, word: str) -> float:
|
|
38
|
+
"""
|
|
39
|
+
:param word: a word
|
|
40
|
+
:type word: str
|
|
41
|
+
"""
|
|
42
|
+
return self.frequencies[word] / sum(self.frequencies.values())
|
|
43
|
+
|
|
44
|
+
def correction(self, word: str) -> str:
|
|
45
|
+
"""
|
|
46
|
+
:param word: a word
|
|
47
|
+
:type word: str
|
|
48
|
+
:return: most probable spelling correction for word
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# Preserve
|
|
52
|
+
word_isupper = word[0].isupper()
|
|
53
|
+
word = word.lower()
|
|
54
|
+
|
|
55
|
+
def reconstruct_case(word: str, word_isupper: bool) -> str:
|
|
56
|
+
"""
|
|
57
|
+
:param word: the word
|
|
58
|
+
:type word: str
|
|
59
|
+
:param word_isupper: the initial capitalization
|
|
60
|
+
:type word_isupper: bool
|
|
61
|
+
:return: the word with its initial capitalization
|
|
62
|
+
"""
|
|
63
|
+
if word_isupper:
|
|
64
|
+
return word.capitalize()
|
|
65
|
+
else:
|
|
66
|
+
return word
|
|
67
|
+
|
|
68
|
+
if word in self.cache:
|
|
69
|
+
return reconstruct_case(self.cache[word], word_isupper)
|
|
70
|
+
else:
|
|
71
|
+
correction = max(self.candidates(word), key=self.p_word)
|
|
72
|
+
|
|
73
|
+
if self.caching:
|
|
74
|
+
self.cache[word] = correction
|
|
75
|
+
|
|
76
|
+
return reconstruct_case(correction, word_isupper)
|
|
77
|
+
|
|
78
|
+
def candidates(self, word: str) -> set[str] | list[str]:
|
|
79
|
+
"""
|
|
80
|
+
:param word: a word
|
|
81
|
+
:type word: str
|
|
82
|
+
:return: a list of candidates
|
|
83
|
+
"""
|
|
84
|
+
return (
|
|
85
|
+
self.known([word])
|
|
86
|
+
or self.known(self.edit_distance_1(word))
|
|
87
|
+
or self.known(self.edit_distance_2(word))
|
|
88
|
+
or [word]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def known(self, words: Iterable[str]) -> set[str]:
|
|
92
|
+
"""
|
|
93
|
+
:param word: a word
|
|
94
|
+
:type word: str
|
|
95
|
+
:return: a subset of words in the dictionary of frequencies
|
|
96
|
+
"""
|
|
97
|
+
return {w for w in words if w in self.frequencies}
|
|
98
|
+
|
|
99
|
+
def edit_distance_1(self, word: str) -> set[str]:
|
|
100
|
+
"""
|
|
101
|
+
:param word: a word
|
|
102
|
+
:type word: str
|
|
103
|
+
:return: all edits one edit away from the word
|
|
104
|
+
"""
|
|
105
|
+
letters = 'abcdefghijklmnopqrstuvwxyz'
|
|
106
|
+
splits = [(word[:i], word[i:]) for i in range(len(word) + 1)]
|
|
107
|
+
deletes = [L + R[1:] for L, R in splits if R]
|
|
108
|
+
transposes = [L + R[1] + R[0] + R[2:] for L, R in splits if len(R) > 1]
|
|
109
|
+
replaces = [L + c + R[1:] for L, R in splits if R for c in letters]
|
|
110
|
+
inserts = [L + c + R for L, R in splits for c in letters]
|
|
111
|
+
|
|
112
|
+
return set(deletes + transposes + replaces + inserts)
|
|
113
|
+
|
|
114
|
+
def edit_distance_2(self, word: str) -> Iterator[str]:
|
|
115
|
+
"""
|
|
116
|
+
:param word: a word
|
|
117
|
+
:type word: str
|
|
118
|
+
:return: all edits two edits away from the word
|
|
119
|
+
"""
|
|
120
|
+
return (e2 for e1 in self.edit_distance_1(word) for e2 in self.edit_distance_1(e1))
|
|
121
|
+
|
|
122
|
+
def correct_string(self, string: str, return_corrections: bool = False) -> Any:
|
|
123
|
+
"""
|
|
124
|
+
:param string: the string to correct.
|
|
125
|
+
:type string: str
|
|
126
|
+
:param return_corrections: include the corrections in the result
|
|
127
|
+
:type return_corrections: bool
|
|
128
|
+
:return: the corrected string
|
|
129
|
+
"""
|
|
130
|
+
corrections: list[tuple[str, str]] = []
|
|
131
|
+
corrected: list[str] = []
|
|
132
|
+
for word in string.split():
|
|
133
|
+
word_matches = re.findall(r'\w+', word)
|
|
134
|
+
|
|
135
|
+
# Tokens without any word characters (e.g. '---') are kept as-is
|
|
136
|
+
if not word_matches:
|
|
137
|
+
corrected.append(word)
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
corrected_word = self.correction(word_matches[0])
|
|
141
|
+
corrected_word = re.sub(r'(.*?)(\w+)(.*?)', rf'\g<1>{corrected_word}\g<3>', word)
|
|
142
|
+
corrected.append(corrected_word)
|
|
143
|
+
|
|
144
|
+
if return_corrections and corrected_word != word:
|
|
145
|
+
corrections.append((word, corrected_word))
|
|
146
|
+
|
|
147
|
+
if return_corrections:
|
|
148
|
+
return (' '.join(corrected), corrections)
|
|
149
|
+
else:
|
|
150
|
+
return ' '.join(corrected)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def generate_crudespellchecker_lm(corpus_directory: str | Path, model_name: str, strip_xml: bool = False) -> None:
|
|
154
|
+
"""
|
|
155
|
+
:param corpus_directory: path the folder containing the files.
|
|
156
|
+
:type corpus_directory: str
|
|
157
|
+
:param model_name: th name of the model
|
|
158
|
+
:type model_name: str
|
|
159
|
+
:param strip_xml: stripping XML tags with bs4
|
|
160
|
+
:type strip_xml: bool
|
|
161
|
+
"""
|
|
162
|
+
from bs4 import BeautifulSoup
|
|
163
|
+
|
|
164
|
+
frequencies: Counter[str] = Counter()
|
|
165
|
+
files = list(Path(corpus_directory).glob('*.txt'))
|
|
166
|
+
|
|
167
|
+
for file in files:
|
|
168
|
+
with open(file, encoding='utf-8', errors='ignore') as corpus_file:
|
|
169
|
+
if strip_xml:
|
|
170
|
+
soup = BeautifulSoup(corpus_file.read().lower(), 'lxml')
|
|
171
|
+
text = soup.get_text()
|
|
172
|
+
else:
|
|
173
|
+
text = corpus_file.read().lower()
|
|
174
|
+
|
|
175
|
+
file_frequency = Counter(re.findall(r'\b[^\d\W]+\b', text))
|
|
176
|
+
frequencies = frequencies + file_frequency
|
|
177
|
+
|
|
178
|
+
with gzip.open(model_name + '.gz.lm', 'wb') as pkl:
|
|
179
|
+
pickle.dump(frequencies, pkl)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# A fairly conservative stopwords list based on frequency with a strong focus on function words. Verbs have been excluded. (13.06.2019)
|
|
2
|
+
a
|
|
3
|
+
about
|
|
4
|
+
above
|
|
5
|
+
an
|
|
6
|
+
and
|
|
7
|
+
as
|
|
8
|
+
at
|
|
9
|
+
because
|
|
10
|
+
been
|
|
11
|
+
below
|
|
12
|
+
between
|
|
13
|
+
but
|
|
14
|
+
by
|
|
15
|
+
could
|
|
16
|
+
for
|
|
17
|
+
from
|
|
18
|
+
he
|
|
19
|
+
her
|
|
20
|
+
hers
|
|
21
|
+
herself
|
|
22
|
+
him
|
|
23
|
+
himself
|
|
24
|
+
his
|
|
25
|
+
how
|
|
26
|
+
i
|
|
27
|
+
if
|
|
28
|
+
in
|
|
29
|
+
into
|
|
30
|
+
it
|
|
31
|
+
its
|
|
32
|
+
itself
|
|
33
|
+
just
|
|
34
|
+
mine
|
|
35
|
+
more
|
|
36
|
+
most
|
|
37
|
+
my
|
|
38
|
+
myself
|
|
39
|
+
not
|
|
40
|
+
of
|
|
41
|
+
on
|
|
42
|
+
or
|
|
43
|
+
our
|
|
44
|
+
ours
|
|
45
|
+
ourselves
|
|
46
|
+
out
|
|
47
|
+
she
|
|
48
|
+
should
|
|
49
|
+
so
|
|
50
|
+
some
|
|
51
|
+
such
|
|
52
|
+
the
|
|
53
|
+
their
|
|
54
|
+
theirs
|
|
55
|
+
them
|
|
56
|
+
themselves
|
|
57
|
+
these
|
|
58
|
+
they
|
|
59
|
+
those
|
|
60
|
+
through
|
|
61
|
+
to
|
|
62
|
+
us
|
|
63
|
+
very
|
|
64
|
+
we
|
|
65
|
+
what
|
|
66
|
+
when
|
|
67
|
+
which
|
|
68
|
+
while
|
|
69
|
+
who
|
|
70
|
+
would
|
|
71
|
+
you
|
|
72
|
+
your
|
|
73
|
+
yours
|
|
74
|
+
yourself
|
|
75
|
+
yourselves
|
|
76
|
+
this
|
|
77
|
+
that
|
|
78
|
+
whose
|
|
79
|
+
both
|
|
80
|
+
many
|
|
81
|
+
much
|
|
82
|
+
enough
|
|
83
|
+
several
|
|
84
|
+
yet
|
|
85
|
+
neither
|
|
86
|
+
although
|
|
87
|
+
however
|
|
88
|
+
before
|
|
89
|
+
over
|
|
90
|
+
across
|
|
91
|
+
around
|
|
92
|
+
within
|
|
93
|
+
really
|
|
94
|
+
quite
|
|
95
|
+
somewhat
|
|
96
|
+
rather
|
textdirectory/helpers.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Helpers module."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import re
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def tabulate_flat_list_of_dicts(list_of_dicts: list[dict[str, Any]], max_length: int = 25) -> str:
|
|
11
|
+
"""
|
|
12
|
+
:param list_of_dicts: a list of dictionaries; each list is a row
|
|
13
|
+
:type list_of_dicts: list
|
|
14
|
+
:param max_length: the maximum length of a cell
|
|
15
|
+
:type max_length: int
|
|
16
|
+
:return: a table
|
|
17
|
+
:type return: str
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# Create a copy of the list to prevent object mutation
|
|
21
|
+
list_of_dicts = copy.deepcopy(list_of_dicts)
|
|
22
|
+
|
|
23
|
+
if len(list_of_dicts) == 0:
|
|
24
|
+
return ''
|
|
25
|
+
|
|
26
|
+
# Enforce a maximum length
|
|
27
|
+
if max_length:
|
|
28
|
+
for row in list_of_dicts:
|
|
29
|
+
for key, value in row.items():
|
|
30
|
+
row[key] = str(value)[:max_length]
|
|
31
|
+
|
|
32
|
+
# Determine the width of the columns
|
|
33
|
+
longest_values: dict[str, int] = {}
|
|
34
|
+
|
|
35
|
+
for key in list_of_dicts[0].keys():
|
|
36
|
+
longest_values[key] = len(key)
|
|
37
|
+
|
|
38
|
+
for row in list_of_dicts:
|
|
39
|
+
for key, value in row.items():
|
|
40
|
+
value = str(value)
|
|
41
|
+
if key in longest_values:
|
|
42
|
+
if len(value) > longest_values[key]:
|
|
43
|
+
longest_values[key] = len(value)
|
|
44
|
+
else:
|
|
45
|
+
longest_values[key] = len(value)
|
|
46
|
+
|
|
47
|
+
# Line / len(longest_values) = additonal characters for pipes
|
|
48
|
+
length = 0
|
|
49
|
+
for value in longest_values.values():
|
|
50
|
+
length += value
|
|
51
|
+
|
|
52
|
+
line = '\n|' + '-' * (length + len(longest_values) - 1) + '|'
|
|
53
|
+
|
|
54
|
+
# Header based on the first dictionary
|
|
55
|
+
table = line + '\n|'
|
|
56
|
+
for key in list_of_dicts[0].keys():
|
|
57
|
+
table += f'{key}'.ljust(longest_values[key]) + '|'
|
|
58
|
+
|
|
59
|
+
table += line
|
|
60
|
+
|
|
61
|
+
# Rows
|
|
62
|
+
for row in list_of_dicts:
|
|
63
|
+
table += '\n|'
|
|
64
|
+
for key, value in row.items():
|
|
65
|
+
# Remove linebreaks
|
|
66
|
+
value = value.replace('\n', '')
|
|
67
|
+
table += str(value).ljust(longest_values[key]) + '|'
|
|
68
|
+
|
|
69
|
+
table += line
|
|
70
|
+
|
|
71
|
+
return table
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def count_non_alphanum(string: str) -> int:
|
|
75
|
+
"""
|
|
76
|
+
:param string: a string
|
|
77
|
+
:type string: str
|
|
78
|
+
:return: the number of non-alphanumeric characters in the string
|
|
79
|
+
:type return: int
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
non_alphanum = 0
|
|
83
|
+
for c in string:
|
|
84
|
+
if not c.isalpha():
|
|
85
|
+
non_alphanum += 1
|
|
86
|
+
|
|
87
|
+
return non_alphanum
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def chunk_text(string: str, chunk_size: int = 50000) -> list[str]:
|
|
91
|
+
"""
|
|
92
|
+
:param string: a string
|
|
93
|
+
:type string: str
|
|
94
|
+
:param chunk_size: the max characters of one chunk
|
|
95
|
+
:type chunk_size: int
|
|
96
|
+
:return: a list of chunks
|
|
97
|
+
:type return: list
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
chunks = [string[i : i + chunk_size] for i in range(0, len(string), chunk_size)]
|
|
101
|
+
return chunks
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def simple_tokenizer(string: str, regular_expression: str = r'\w+') -> list[str]:
|
|
105
|
+
"""
|
|
106
|
+
:param string: a string
|
|
107
|
+
:type string: str
|
|
108
|
+
:return: a list of tokens
|
|
109
|
+
:type return: list
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
if not regular_expression:
|
|
113
|
+
tokens = string.split(' ')
|
|
114
|
+
else:
|
|
115
|
+
tokens = re.findall(regular_expression, string)
|
|
116
|
+
|
|
117
|
+
return tokens
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def estimate_spacy_max_length(override: float | bool = False, tokenizer_only: bool = False) -> float:
|
|
121
|
+
"""Returns a somewhat sensible suggestions for max_length."""
|
|
122
|
+
if override:
|
|
123
|
+
return override
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
import psutil
|
|
127
|
+
except ImportError as e:
|
|
128
|
+
raise ImportError(
|
|
129
|
+
'estimate_spacy_max_length requires psutil, which is an optional dependency. '
|
|
130
|
+
"Install it with: pip install 'textdirectory[nlp]'"
|
|
131
|
+
) from e
|
|
132
|
+
|
|
133
|
+
memory = psutil.virtual_memory()
|
|
134
|
+
gb_available = memory.available / 1024 / 1024 / 1024
|
|
135
|
+
|
|
136
|
+
# tagger, parser, ner 100,000 characters = 1 GB
|
|
137
|
+
estimated_max_length = gb_available * 100000
|
|
138
|
+
|
|
139
|
+
if tokenizer_only:
|
|
140
|
+
estimated_max_length = estimated_max_length * 3
|
|
141
|
+
|
|
142
|
+
return estimated_max_length
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def type_token_ratio(text: str) -> float:
|
|
146
|
+
"""Returns a simple rounded type-token ratio of a text."""
|
|
147
|
+
tokens = simple_tokenizer(text)
|
|
148
|
+
no_types = len(Counter(tokens))
|
|
149
|
+
no_tokens = len(tokens)
|
|
150
|
+
|
|
151
|
+
if no_tokens == 0:
|
|
152
|
+
return 0.0
|
|
153
|
+
|
|
154
|
+
return round(no_types / no_tokens, 2)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_human_from_docstring(doc: str) -> dict[str, str]:
|
|
158
|
+
"""
|
|
159
|
+
:param doc: if True, also return the 'human name'
|
|
160
|
+
:type doc: string
|
|
161
|
+
:return: a dictionary of name_* keys/values from the docstring.
|
|
162
|
+
:type return: dict
|
|
163
|
+
"""
|
|
164
|
+
doc = doc.replace(' ', '')
|
|
165
|
+
res = re.findall(r'human_(.*):(.*)', doc)
|
|
166
|
+
|
|
167
|
+
return {k: v.strip() for (k, v) in res}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def coerce_args_by_signature(func: Callable[..., Any], args: list[Any]) -> list[Any]:
|
|
171
|
+
"""
|
|
172
|
+
Coerce string arguments to the types suggested by a function's signature.
|
|
173
|
+
|
|
174
|
+
String values are converted to int, float, or bool when the corresponding
|
|
175
|
+
parameter's annotation or default value indicates such a type; all other
|
|
176
|
+
values are passed through unchanged.
|
|
177
|
+
|
|
178
|
+
:param func: the function or method the arguments are meant for
|
|
179
|
+
:type func: callable
|
|
180
|
+
:param args: the arguments to coerce
|
|
181
|
+
:type args: list
|
|
182
|
+
:return: a list of coerced arguments
|
|
183
|
+
:type return: list
|
|
184
|
+
"""
|
|
185
|
+
import inspect
|
|
186
|
+
|
|
187
|
+
parameters = [p for p in inspect.signature(func).parameters.values() if p.name not in ('self', 'text')]
|
|
188
|
+
|
|
189
|
+
coerced: list[Any] = []
|
|
190
|
+
for arg, parameter in zip(args, parameters, strict=False):
|
|
191
|
+
target_type = None
|
|
192
|
+
|
|
193
|
+
if parameter.annotation in (int, float, bool):
|
|
194
|
+
target_type = parameter.annotation
|
|
195
|
+
elif parameter.default is not inspect.Parameter.empty and isinstance(parameter.default, (bool, int, float)):
|
|
196
|
+
target_type = type(parameter.default)
|
|
197
|
+
|
|
198
|
+
if not isinstance(arg, str) or target_type is None:
|
|
199
|
+
coerced.append(arg)
|
|
200
|
+
elif target_type is bool:
|
|
201
|
+
coerced.append(arg.lower() in ('1', 'true', 'yes'))
|
|
202
|
+
else:
|
|
203
|
+
try:
|
|
204
|
+
coerced.append(target_type(arg))
|
|
205
|
+
except ValueError:
|
|
206
|
+
coerced.append(arg)
|
|
207
|
+
|
|
208
|
+
# Surplus arguments (consumed by *args) are passed through untouched
|
|
209
|
+
coerced.extend(args[len(coerced) :])
|
|
210
|
+
|
|
211
|
+
return coerced
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def get_available_filters(get_human_name: bool = False) -> list[Any]:
|
|
215
|
+
"""
|
|
216
|
+
:param get_human_name: if True, also return the 'human name'
|
|
217
|
+
:type get_human_name: bool
|
|
218
|
+
:return: a list of functions; if get_human_name a list of tuples
|
|
219
|
+
:type return: list
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
from textdirectory.textdirectory import TextDirectory
|
|
223
|
+
|
|
224
|
+
available_filters: list[Any] = [
|
|
225
|
+
filter
|
|
226
|
+
for filter in dir(TextDirectory)
|
|
227
|
+
if filter.startswith('filter_by_') and callable(getattr(TextDirectory, filter))
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
if get_human_name:
|
|
231
|
+
available_filters_with_human: list[tuple[str, str]] = []
|
|
232
|
+
for f in available_filters:
|
|
233
|
+
doc = getattr(TextDirectory, f).__doc__
|
|
234
|
+
human = get_human_from_docstring(doc)
|
|
235
|
+
if 'name' in human:
|
|
236
|
+
available_filters_with_human.append((f, human['name']))
|
|
237
|
+
else:
|
|
238
|
+
available_filters_with_human.append((f, f))
|
|
239
|
+
|
|
240
|
+
available_filters = available_filters_with_human
|
|
241
|
+
|
|
242
|
+
return available_filters
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def get_available_transformations(get_human_name: bool = False) -> list[Any]:
|
|
246
|
+
"""
|
|
247
|
+
:param get_human_name: if True, also return the 'human name'
|
|
248
|
+
:type string: bool
|
|
249
|
+
:return: a list of functions; if get_human_name a list of tuples
|
|
250
|
+
:type return: list
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
from textdirectory import transformations
|
|
254
|
+
|
|
255
|
+
available_transformations: list[Any] = [
|
|
256
|
+
transformation
|
|
257
|
+
for transformation in dir(transformations)
|
|
258
|
+
if transformation.startswith('transformation_') and callable(getattr(transformations, transformation))
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
if get_human_name:
|
|
262
|
+
available_transformations_with_human: list[tuple[str, str]] = []
|
|
263
|
+
for t in available_transformations:
|
|
264
|
+
doc = getattr(transformations, t).__doc__
|
|
265
|
+
human = get_human_from_docstring(doc)
|
|
266
|
+
if 'name' in human:
|
|
267
|
+
available_transformations_with_human.append((t, human['name']))
|
|
268
|
+
else:
|
|
269
|
+
available_transformations_with_human.append((t, t))
|
|
270
|
+
|
|
271
|
+
available_transformations = available_transformations_with_human
|
|
272
|
+
|
|
273
|
+
return available_transformations
|
textdirectory/py.typed
ADDED
|
File without changes
|