blob: 06ea564874025b577f57af93955927f1258b6506 [file] [log] [blame]
Marc Kupietz86044852025-11-29 10:19:03 +01001from collections import defaultdict, OrderedDict
2import re
3
4# CoNLL-U Format - https://universaldependencies.org/format.html
5
6
7def get_token_type(type_str):
8 if type_str =="CoNLL09_Token":
9 return CoNLL09_Token
10 elif type_str == "RNNTagger_Token":
11 return RNNTagger_Token
12 elif type_str == "CoNLLUP_Token":
13 return CoNLLUP_Token
14 elif type_str == "TigerNew_Token":
15 return TigerNew_Token
16 else:
17 raise NotImplementedError(f"I don't know what to do with {type_str} token type!")
18
19
20class TigerNew_Token():
21 def __init__(self, raw_line, word_ix):
22 info = raw_line.split() # [FORM, XPOS]
23 self.info = info
24 self.id = word_ix + 1 # 1-based ID as in the CoNLL file
25 self.position = word_ix # 0-based position in sentence
26 self.word = info[0]
27 self.lemma = "_"
28 self.pos_universal = "_"
29 self.pos_tag = info[1]
30 self.detail_tag = "_"
31 self.head = "_"
32 self.dep_tag = "_"
33 self.blank = "_"
34 self.auto_score = "_"
35
36 def get_info(self):
37 return [str(self.id), self.word, self.lemma, self.pos_universal, self.pos_tag, self.detail_tag,
38 str(self.head), self.dep_tag, self.blank, self.auto_score]
39
40 def get_conllU_line(self, separator="\t"):
41 info = self.get_info()
42 return separator.join(info)
43
44
45class RNNTagger_Token():
46 def __init__(self, raw_line, word_ix):
47 info = raw_line.split() # [FORM, XPOS.FEATS, LEMMA]
48 self.info = info
49 self.id = word_ix + 1 # 1-based ID as in the CoNLL file
50 self.position = word_ix # 0-based position in sentence
51 self.word = info[0]
52 self.lemma = info[2]
53 self.pos_universal = "_"
54 self.pos_tag, self.detail_tag = self._process_tag(info[1]) # 'NN.Gen.Sg.Fem'
55 self.head = "_"
56 self.dep_tag = "_"
57 self.blank = "_"
58 self.auto_score = "_"
59
60 def _process_tag(self, tag):
61 if tag == "_" or "." not in tag: return tag, "_"
62 info = tag.split(".")
63 return info[0], "|".join(info[1:])
64
65 def get_info(self):
66 return [str(self.id), self.word, self.lemma, self.pos_universal, self.pos_tag, self.detail_tag,
67 str(self.head), self.dep_tag, self.blank, self.auto_score]
68
69 def get_conllU_line(self, separator="\t"):
70 info = self.get_info()
71 return separator.join(info)
72
73
74class CoNLLUP_Token():
75 def __init__(self, raw_line, word_ix):
Marc Kupietzffad7372026-06-19 20:36:34 +020076 # CoNLL-U is tab-separated with 10 columns. Split on the tab so that a
77 # token whose FORM is whitespace (e.g. a surface form that is a single
78 # space) keeps its column position instead of collapsing it -- a bare
79 # raw_line.split() would drop the empty FORM field, shift every column
80 # left, and raise IndexError on info[9], which crashes the whole
81 # streaming process and cascades into broken pipes across the worker
82 # pool. Fall back to a generic whitespace split for space-delimited
83 # inputs, then pad to 10 columns so a genuinely malformed/short line
84 # degrades gracefully rather than killing the stream.
85 info = raw_line.rstrip("\n").split("\t")
86 if len(info) < 10:
87 info = raw_line.split()
88 if len(info) < 10:
89 info = info + ["_"] * (10 - len(info))
Marc Kupietz86044852025-11-29 10:19:03 +010090 # print(info)
91 # [ID, FORM, LEMMA, UPOS, XPOS, FEATS, HEAD, DEPREL, DEPS, MISC]
92 # [11, Prügel, Prügel, NN, NN, _, _, _, _, 1.000000]
93 self.info = info
94 self.id = info[0] # 1-based ID as in the CoNLL file
95 self.position = word_ix # 0-based position in sentence
96 self.word = info[1]
97 self.lemma = info[2]
98 self.pos_universal = info[3]
99 self.pos_tag = self._process_tag(info[4]) # 'XPOS=NE|Case=Nom|Gender=Masc|Number=Sing' TODO: Reuse MorphInfo in the self.detail_tag
100 self.detail_tag = info[5]
101 self.head = info[6]
102 self.dep_tag = info[7]
103 self.blank = info[8] # ???
104 self.auto_score = info[9]
105
106 def _process_tag(self, tag):
107 if tag == "_" or "|" not in tag: return tag # The XPOS=NE|Case=Nom... is only for Turku!
108 info = tag.split("|")
109 info = [x.split("=") for x in info]
110 return info[0][1]
111
112 def get_info(self):
113 return [str(self.id), self.word, self.lemma, self.pos_universal, self.pos_tag, self.detail_tag,
114 str(self.head), self.dep_tag, self.blank, self.auto_score]
115
116 def get_conllU_line(self, separator="\t"):
117 info = self.get_info()
118 return separator.join(info)
119
120
121
122class CoNLL09_Token():
123 def __init__(self, raw_line, word_ix):
124 info = raw_line.split()
125 # print(info)
126 # # ['1', 'Frau', 'Frau', 'Frau', 'NN', 'NN', '_', 'nom|sg|fem', '5', '5', 'CJ', 'CJ', '_', '_', 'AM-DIS', '_']
127 self.info = info
128 self.id = info[0] # 1-based ID as in the CoNLL file
129 self.position = word_ix # 0-based position in sentence
130 self.word = info[1]
131 self.lemma = info[2]
132 self.pos_universal = "_" # _convert_to_universal(self.pos_tag, self.lemma)
133 self.pos_tag = info[4]
134 self.head = info[8]
135 self.dep_tag = info[10]
136 self.detail_tag = "_"
137 self.is_pred = True if info[12] == "Y" else False
138 if self.is_pred:
139 self.pred_sense = info[13].strip("[]")
140 self.pred_sense_id = str(self.position) + "##" + self.pred_sense
141 else:
142 self.pred_sense = None
143 self.pred_sense_id = ""
144 if len(info) > 14:
145 self.labels = info[14:]
146 else:
147 self.labels = []
148
149 def get_conllU_line(self, separator="\t"):
150 # We want: [ID, FORM, LEMMA, UPOS, XPOS, FEATS, HEAD, DEPREL, DEPS, MISC]
151 tok_id = str(self.id) #.split("_")[0]
152 conllUinfo = [tok_id, self.word, self.lemma, self.pos_universal, self.pos_tag, self.detail_tag, self.head, self.dep_tag, "_", "_"]
153 return separator.join(conllUinfo)
154
155 def get_conll09_line(self, delim="\t"):
156 # We want:
157 # 1 Frau Frau Frau NN NN _ nom|sg|fem 5 5 CJ CJ _ _ AM-DIS _
158 # 10 fall fall fall VB VB _ _ 8 8 VC VC Y fall.01 _ _ _ _ _
159 is_pred_str = "Y" if self.is_pred else "_"
160 sense_str = self.pred_sense if self.is_pred else "_"
161 info = [self.id, self.word, self.lemma, self.lemma, self.pos_tag, self.pos_tag, "_", self.detail_tag,
162 self.head, self.head, self.dep_tag, self.dep_tag, is_pred_str, sense_str] + self.labels
163 return delim.join(info)
164
165
166
167################################# GETTING SENTENCE ANNOTATIONS ####################################
168class AnnotatedSentence():
169 def __init__(self):
170 self.metadata = []
171 self.tokens = []
172
173 def get_words(self):
174 return [tok.word for tok in self.tokens]
175
176 def get_sentence(self):
177 return " ".join([tok.word for tok in self.tokens])
178
179 def get_pos_tags(self, universal=False):
180 if universal:
181 return [tok.pos_universal for tok in self.tokens]
182 else:
183 return [tok.pos_tag for tok in self.tokens]
184
185
186def get_annotation(raw_lines, raw_meta, token_class):
187 ann = AnnotatedSentence()
188 ann.metadata = [m.strip("\n") for m in raw_meta]
189 # Annotate the predicates and senses
190 real_index = 0
191 for i, line in enumerate(raw_lines):
192 tok = token_class(line, real_index)
193 ann.tokens.append(tok)
194 real_index += 1
195 return ann
196
197
198def read_conll(line_generator, chunk_size, token_class=CoNLLUP_Token, comment_str="###C:", our_foundry="spacy"):
199 n_sents = 0
200 annotated_sentences, buffer_meta, buffer_lst = [], [], []
201 for i, line in enumerate(line_generator):
202 if line.startswith(comment_str):
203 line = re.sub(r'(foundry\s*=\s*).*', r"\1" + our_foundry, line)
204 line = re.sub(r'(filename\s*=\s* .[^/]*/[^/]+/[^/]+/).*', r"\1" + our_foundry + "/morpho.xml", line)
205 buffer_meta.append(line)
206 continue
207 if len(line.split()) > 0:
208 buffer_lst.append(line)
209 else:
210 ann = get_annotation(buffer_lst, buffer_meta, token_class)
211 n_sents += 1
212 buffer_lst, buffer_meta = [], []
213 annotated_sentences.append(ann)
214 if chunk_size > 0 and n_sents == chunk_size: break
215 # logger.info("Read {} Sentences!".format(n_sents))
216 return annotated_sentences, n_sents
217
218
219def read_conll_generator(filepath, token_class=CoNLLUP_Token, sent_sep=None, comment_str="###C:"):
220 buffer_meta, buffer_lst = [], []
221 sentence_finished = False
222 with open(filepath) as f:
223 for i, line in enumerate(f.readlines()):
224 if sent_sep and sent_sep in line: sentence_finished = True
225 if line.startswith(comment_str):
226 continue
227 if len(line.split()) > 0 and not sentence_finished:
228 buffer_lst.append(line)
229 else:
230 ann = get_annotation(buffer_lst, buffer_meta, token_class)
231 buffer_lst, buffer_meta = [], []
232 sentence_finished = False
233 yield ann