22# outputs a JSONL dataset for NER model training
33import hashlib
44import json
5- import re
65import unicodedata
76from collections import Counter
87from pathlib import Path
1413from licensedcode .tokenize import required_phrase_splitter
1514
1615
17- def normalize_phrase (phrase ):
18- """Clean raw marker phrase for training"""
19- result = phrase
20- # replace html entities
21- result = result .replace ('"' , '"' ).replace ('&' , '&' )
22- result = result .replace ('<' , '<' ).replace ('>' , '>' )
23- # strip xml tags like <name>,</license> but keep urls in angle brackets
24- result = re .sub (r'<(?![a-zA-Z]+://)[^>]+>' , '' , result )
25- # remove markdown backticks
26- result = result .replace ('`' , '' )
27- # collapse whitespace and trim
28- result = re .sub (r'\s+' , ' ' , result ).strip ()
29- # strip trailing/leading punct thats not meaningful
30- result = result .strip ('.,;:<>' )
31- return result
32-
33-
3416def get_rule_type (rule ):
35- """is_* flag set on the rule"""
17+ """Return the is_* flag set on the rule"""
3618 for flag in ('is_license_text' , 'is_license_notice' , 'is_license_reference' ,
3719 'is_license_tag' , 'is_license_intro' , 'is_license_clue' ,
3820 'is_false_positive' ):
@@ -42,11 +24,11 @@ def get_rule_type(rule):
4224
4325
4426def tag_tokens (text ):
45- """Tag each word token with a BIOES label"""
27+ """Tag each word token with a BIOES label based on {{ }} markers """
4628 tokens = []
4729 labels = []
4830 in_phrase = False
49- count = 0 # word tokens seen since the last {{
31+ count = 0
5032
5133 for tok in required_phrase_splitter (text ):
5234 if tok == '{{' :
@@ -65,16 +47,18 @@ def tag_tokens(text):
6547 count += 1
6648 else :
6749 labels .append ('O' )
50+
51+ assert len (tokens ) == len (labels ), f'token/label mismatch: { len (tokens )} vs { len (labels )} '
6852 return tokens , labels
6953
7054
7155def assign_splits (results , threshold = 50 ):
72- """80/10/10 split. common expressions (>= threshold rules) get split per-rule,
56+ """80/10/10 split by license expression to prevent data leakage.
57+ Expressions with >= threshold rules get split per-rule via hash,
7358 rare ones stay together in one split"""
7459 expr_counts = Counter (e ['license_expression' ] for e in results )
7560 heavy = {e for e , c in expr_counts .items () if c >= threshold }
7661
77- # rare expressions: assign each to the split that needs more rules
7862 light_exprs = sorted ((e for e in expr_counts if e not in heavy ),
7963 key = lambda x : (- expr_counts [x ], x ))
8064 total = sum (expr_counts [e ] for e in light_exprs )
@@ -106,18 +90,17 @@ def main(rules_dir, output_dir):
10690
10791 total_rules = 0
10892 annotated = 0
109- total_phrases = 0
11093 results = []
11194
11295 click .echo (f'scanning rules from: { rules_path } ' )
11396 for rf in sorted (rules_path .glob ('*.RULE' )):
11497 try :
11598 rule = Rule .from_file (rule_file = str (rf ))
116- except Exception :
99+ except Exception as e :
100+ click .echo (f' skipping { rf .name } : { e } ' , err = True )
117101 continue
118102 total_rules += 1
119103
120- # is_required_phrase rules don't need {{ }}.the flag covers them
121104 if getattr (rule , 'is_required_phrase' , False ):
122105 continue
123106
@@ -133,27 +116,19 @@ def main(rules_dir, output_dir):
133116 if not phrases :
134117 continue
135118
136- # word tokens + BIOES labels (computed before stripping markers)
137119 tokens , bioes_labels = tag_tokens (text )
138120
139- # strip out the {{ }} markers
140- text = text .replace ('{{' , '' ).replace ('}}' , '' )
141-
142- valid_phrases = [
143- {'phrase' : p , 'phrase_normalized' : normalize_phrase (p )}
144- for p in phrases
145- ]
121+ # strip markers for the clean text field
122+ clean_text = text .replace ('{{' , '' ).replace ('}}' , '' )
146123
147124 annotated += 1
148- total_phrases += len (valid_phrases )
149125 results .append ({
150126 'identifier' : rule .identifier ,
151127 'license_expression' : rule .license_expression or '' ,
152128 'rule_type' : get_rule_type (rule ),
153- 'text' : text ,
129+ 'text' : clean_text ,
154130 'tokens' : tokens ,
155131 'bioes_labels' : bioes_labels ,
156- 'required_phrases' : valid_phrases ,
157132 })
158133
159134 # split by license expression and write
@@ -162,7 +137,6 @@ def main(rules_dir, output_dir):
162137 for entry in results :
163138 expr = entry ['license_expression' ]
164139 if expr in heavy :
165- # common expressions: hash rule name for 80/10/10
166140 bucket = int (hashlib .md5 (entry ['identifier' ].encode ('utf-8' )).hexdigest (), 16 ) % 100
167141 if bucket < 80 :
168142 splits ['train' ].append (entry )
@@ -182,7 +156,6 @@ def main(rules_dir, output_dir):
182156 click .echo ('\n done' )
183157 click .echo (f' rules scanned: { total_rules } ' )
184158 click .echo (f' annotated: { annotated } ' )
185- click .echo (f' phrases extracted: { total_phrases } ' )
186159 click .echo (f' train: { len (splits ["train" ])} val: { len (splits ["val" ])} test: { len (splits ["test" ])} ' )
187160 click .echo (f' output: { out_dir } ' )
188161
0 commit comments