项目文件夹

文件
2026-07-13 12:37:18 +08:00

321 行
11 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding:utf-8 -*-
# Author: hankcs
# Date: 2020-11-25 16:14
import os
import shutil
import sys
from collections import defaultdict
from os import listdir
from os.path import join, isfile
from typing import List
from phrasetree.tree import Tree
from hanlp.components.parsers.conll import read_conll
from hanlp.utils.io_util import get_resource, get_exitcode_stdout_stderr, read_tsv_as_sents, run_cmd, pushd
from hanlp.utils.log_util import cprint
from hanlp.utils.time_util import CountdownTimer
# See Shao et al., 2017
# CTB9_ACADEMIA_SPLITS = {
# 'train': '''
# 0044-0143, 0170-0270, 0400-0899,
# 1001-1017, 1019, 1021-1035, 1037-
# 1043, 1045-1059, 1062-1071, 1073-
# 1117, 1120-1131, 1133-1140, 1143-
# 1147, 1149-1151, 2000-2915, 4051-
# 4099, 4112-4180, 4198-4368, 5000-
# 5446, 6000-6560, 7000-7013
# ''',
# 'dev': '''
# 0301-0326, 2916-3030, 4100-4106,
# 4181-4189, 4369-4390, 5447-5492,
# 6561-6630, 7013-7014
# ''',
# 'test': '''
# 0001-0043, 0144-0169, 0271-0301,
# 0900-0931, 1018, 1020, 1036, 1044,
# 1060, 1061, 1072, 1118, 1119, 1132,
# 1141, 1142, 1148, 3031-3145, 4107-
# 4111, 4190-4197, 4391-4411, 5493-
# 5558, 6631-6700, 7015-7017
# '''
# }
#
#
# def _make_splits(splits: Dict[str, str]):
# total = set()
# for part, text in list(splits.items()):
# if not isinstance(text, str):
# continue
# lines = text.replace('\n', '').split()
# cids = set()
# for line in lines:
# for each in line.split(','):
# each = each.strip()
# if not each:
# continue
# if '-' in each:
# start, end = each.split('-')
# start, end = map(lambda x: int(x), [start, end])
# cids.update(range(start, end + 1))
# # cids.update(map(lambda x: f'{x:04d}', range(start, end)))
# else:
# cids.add(int(each))
# cids = set(f'{x:04d}' for x in cids)
# assert len(cids & total) == 0, f'Overlap found in {part}'
# splits[part] = cids
#
# return splits
#
#
# _make_splits(CTB9_ACADEMIA_SPLITS)
def convert_to_dependency(src, dst, language='zh', version='3.3.0', conllx=True, ud=False):
cprint(f'Converting {os.path.basename(src)} to {os.path.basename(dst)} using Stanford Parser Version {version}. '
f'It might take a while [blink][yellow]...[/yellow][/blink]')
if version == '3.3.0':
sp_home = 'https://nlp.stanford.edu/software/stanford-parser-full-2013-11-12.zip'
elif version == '4.2.0':
sp_home = 'https://nlp.stanford.edu/software/stanford-parser-4.2.0.zip'
else:
raise ValueError(f'Unsupported version {version}')
sp_home = get_resource(sp_home)
# jar_path = get_resource(f'{sp_home}#stanford-parser.jar')
if ud:
jclass = 'edu.stanford.nlp.trees.international.pennchinese.UniversalChineseGrammaticalStructure' if language == 'zh' \
else 'edu.stanford.nlp.trees.ud.UniversalDependenciesConverter'
else:
jclass = 'edu.stanford.nlp.trees.international.pennchinese.ChineseGrammaticalStructure' if language == 'zh' \
else 'edu.stanford.nlp.trees.EnglishGrammaticalStructure'
cmd = f'java -cp {sp_home}/* {jclass} ' \
f'-treeFile {src}'
if conllx:
cmd += ' -conllx'
if not ud:
cmd += f' -basic -keepPunct'
code, out, err = get_exitcode_stdout_stderr(cmd)
with open(dst, 'w') as f:
f.write(out)
if code:
raise RuntimeError(f'Conversion failed with code {code} for {src}. The err message is:\n {err}\n'
f'Do you have java installed? Do you have enough memory?')
def clean_ctb_bracketed(ctb_root, out_root):
os.makedirs(out_root, exist_ok=True)
ctb_root = join(ctb_root, 'bracketed')
chtbs = _list_treebank_root(ctb_root)
timer = CountdownTimer(len(chtbs))
for f in chtbs:
with open(join(ctb_root, f), encoding='utf-8') as src, open(join(out_root, f + '.txt'), 'w',
encoding='utf-8') as out:
for line in src:
if not line.strip().startswith('<'):
out.write(line)
timer.log('Cleaning up CTB [blink][yellow]...[/yellow][/blink]', erase=False)
def _list_treebank_root(ctb_root):
chtbs = [f for f in listdir(ctb_root) if isfile(join(ctb_root, f)) and f.startswith('chtb')]
return sorted(chtbs)
def list_treebank(ctb_home):
ctb_home = get_resource(ctb_home)
cleaned_root = join(ctb_home, 'cleaned_bracket')
return _list_treebank_root(cleaned_root)
def load_bracketed_trees(chtbs) -> List[Tree]:
trees = []
for f in chtbs:
with open(f, encoding='utf-8') as src:
content = src.read()
trees = [x for x in content.split('\n\n') if x.strip()]
for tree in trees:
tree = Tree.fromstring(tree)
trees.append(tree)
return trees
def split_str_to_trees(text: str):
trees = []
buffer = []
for line in text.split('\n'):
if not line.strip():
continue
if line.startswith('('):
if buffer:
trees.append('\n'.join(buffer).strip())
buffer = []
buffer.append(line)
if buffer:
trees.append('\n'.join(buffer).strip())
return trees
def make_ctb_tasks(chtbs, out_root, part):
for task in ['cws', 'pos', 'par', 'dep']:
os.makedirs(join(out_root, task), exist_ok=True)
timer = CountdownTimer(len(chtbs))
par_path = join(out_root, 'par', f'{part}.txt')
with open(join(out_root, 'cws', f'{part}.txt'), 'w', encoding='utf-8') as cws, \
open(join(out_root, 'pos', f'{part}.tsv'), 'w', encoding='utf-8') as pos, \
open(par_path, 'w', encoding='utf-8') as par:
for f in chtbs:
with open(f, encoding='utf-8') as src:
content = src.read()
trees = split_str_to_trees(content)
for tree in trees:
try:
tree = Tree.fromstring(tree)
except ValueError:
print(tree)
exit(1)
words = []
for word, tag in tree.pos():
if tag == '-NONE-' or not tag:
continue
tag = tag.split('-')[0]
if tag == 'X': # 铜_NN 30_CD _X 25_CD _X 14_CD cm_NT 1999_NT
tag = 'FW'
pos.write('{}\t{}\n'.format(word, tag))
words.append(word)
cws.write(' '.join(words))
par.write(tree.pformat(margin=sys.maxsize))
for fp in cws, pos, par:
fp.write('\n')
timer.log(f'Preprocesing the [blue]{part}[/blue] set of CTB [blink][yellow]...[/yellow][/blink]',
erase=False)
remove_all_ec(par_path)
dep_path = join(out_root, 'dep', f'{part}.conllx')
convert_to_dependency(par_path, dep_path)
sents = list(read_conll(dep_path))
with open(dep_path, 'w') as out:
for sent in sents:
for i, cells in enumerate(sent):
tag = cells[3]
tag = tag.split('-')[0] # NT-SHORT ---> NT
if tag == 'X': # 铜_NN 30_CD _X 25_CD _X 14_CD cm_NT 1999_NT
tag = 'FW'
cells[3] = cells[4] = tag
out.write('\t'.join(str(x) for x in cells))
out.write('\n')
out.write('\n')
def reverse_splits(splits):
cid_domain = dict()
for domain, cids in splits.items():
for each in cids:
cid_domain[each] = domain
return cid_domain
def split_chtb(chtbs: List[str], splits=None):
train, dev, test = [], [], []
unused = []
for each in chtbs:
name, domain, ext = each.split('.', 2)
_, cid = name.split('_')
if splits:
if cid in splits['train']:
bin = train
elif cid in splits['dev']:
bin = dev
elif cid in splits['test']:
bin = test
else:
bin = unused
# raise IOError(f'{name} not in any splits')
else:
bin = train
if name.endswith('8'):
bin = dev
elif name.endswith('9'):
bin = test
bin.append(each)
return train, dev, test
def id_of_chtb(each: str):
return int(each.split('.')[0].split('_')[-1])
def make_ctb(ctb_home):
ctb_home = get_resource(ctb_home)
cleaned_root = join(ctb_home, 'cleaned_bracket')
if not os.path.isdir(cleaned_root):
clean_ctb_bracketed(ctb_home, cleaned_root)
tasks_root = join(ctb_home, 'tasks')
if not os.path.isdir(tasks_root):
try:
chtbs = _list_treebank_root(cleaned_root)
print(f'For the {len(chtbs)} files in CTB, we apply the following splits:')
train, dev, test = split_chtb(chtbs)
for part, name in zip([train, dev, test], ['train', 'dev', 'test']):
print(f'{name} = {[id_of_chtb(x) for x in part]}')
cprint('[yellow]Each file id ending with 8/9 is put into '
'dev/test respectively, the rest are put into train. '
'Our splits ensure files are evenly split across each genre, which is recommended '
'for production systems.[/yellow]')
for part, name in zip([train, dev, test], ['train', 'dev', 'test']):
make_ctb_tasks([join(cleaned_root, x) for x in part], tasks_root, name)
cprint('Done pre-processing CTB. Enjoy your research with [blue]HanLP[/blue]!')
except Exception as e:
shutil.rmtree(tasks_root, ignore_errors=True)
raise e
def load_domains(ctb_home):
"""
Load file ids from a Chinese treebank grouped by domains.
Args:
ctb_home: Root path to CTB.
Returns:
A dict of sets, each represents a domain.
"""
ctb_home = get_resource(ctb_home)
ctb_root = join(ctb_home, 'bracketed')
chtbs = _list_treebank_root(ctb_root)
domains = defaultdict(set)
for each in chtbs:
name, domain = each.split('.')
_, fid = name.split('_')
domains[domain].add(fid)
return domains
def ctb_pos_to_text_format(path, delimiter='_'):
"""
Convert ctb pos tagging corpus from tsv format to text format, where each word is followed by
its pos tag.
Args:
path: File to be converted.
delimiter: Delimiter between word and tag.
"""
path = get_resource(path)
name, ext = os.path.splitext(path)
with open(f'{name}.txt', 'w', encoding='utf-8') as out:
for sent in read_tsv_as_sents(path):
out.write(' '.join([delimiter.join(x) for x in sent]))
out.write('\n')
def remove_all_ec(path):
"""
Remove empty categories for all trees in this file and save them into a "noempty" file.
Args:
path: File path.
"""
script = get_resource('https://file.hankcs.com/bin/remove_ec.zip')
with pushd(script):
run_cmd(f'java -cp elit-ddr-0.0.5-SNAPSHOT.jar:elit-sdk-0.0.5-SNAPSHOT.jar:hanlp-1.7.8.jar:'
f'fastutil-8.1.1.jar:. demo.RemoveEmptyCategoriesTreebank {path}')