244 lines
11 KiB
Python
244 lines
11 KiB
Python
|
from collections.abc import Set
|
|||
|
|
|||
|
from dateparser.languages.loader import LocaleDataLoader
|
|||
|
from dateparser.conf import apply_settings, check_settings, Settings
|
|||
|
from dateparser.date import DateDataParser
|
|||
|
from dateparser.search.text_detection import FullTextLanguageDetector
|
|||
|
from dateparser.custom_language_detection.language_mapping import map_languages
|
|||
|
import regex as re
|
|||
|
|
|||
|
|
|||
|
RELATIVE_REG = re.compile("(ago|in|from now|tomorrow|today|yesterday)")
|
|||
|
|
|||
|
|
|||
|
def date_is_relative(translation):
|
|||
|
return re.search(RELATIVE_REG, translation) is not None
|
|||
|
|
|||
|
|
|||
|
class _ExactLanguageSearch:
|
|||
|
def __init__(self, loader):
|
|||
|
self.loader = loader
|
|||
|
self.language = None
|
|||
|
|
|||
|
def get_current_language(self, shortname):
|
|||
|
if self.language is None or self.language.shortname != shortname:
|
|||
|
self.language = self.loader.get_locale(shortname)
|
|||
|
|
|||
|
def search(self, shortname, text, settings):
|
|||
|
self.get_current_language(shortname)
|
|||
|
result = self.language.translate_search(text, settings=settings)
|
|||
|
return result
|
|||
|
|
|||
|
@staticmethod
|
|||
|
def set_relative_base(substring, already_parsed):
|
|||
|
if len(already_parsed) == 0:
|
|||
|
return substring, None
|
|||
|
|
|||
|
i = len(already_parsed) - 1
|
|||
|
while already_parsed[i][1]:
|
|||
|
i -= 1
|
|||
|
if i == -1:
|
|||
|
return substring, None
|
|||
|
relative_base = already_parsed[i][0]['date_obj']
|
|||
|
return substring, relative_base
|
|||
|
|
|||
|
def choose_best_split(self, possible_parsed_splits, possible_substrings_splits):
|
|||
|
rating = []
|
|||
|
for i in range(len(possible_parsed_splits)):
|
|||
|
num_substrings = len(possible_substrings_splits[i])
|
|||
|
num_substrings_without_digits = 0
|
|||
|
not_parsed = 0
|
|||
|
for j, item in enumerate(possible_parsed_splits[i]):
|
|||
|
if item[0]['date_obj'] is None:
|
|||
|
not_parsed += 1
|
|||
|
if not any(char.isdigit() for char in possible_substrings_splits[i][j]):
|
|||
|
num_substrings_without_digits += 1
|
|||
|
rating.append([
|
|||
|
num_substrings,
|
|||
|
0 if not_parsed == 0 else (float(not_parsed) / float(num_substrings)),
|
|||
|
0 if num_substrings_without_digits == 0 else (
|
|||
|
float(num_substrings_without_digits) / float(num_substrings))])
|
|||
|
best_index, best_rating = min(enumerate(rating), key=lambda p: (p[1][1], p[1][0], p[1][2]))
|
|||
|
return possible_parsed_splits[best_index], possible_substrings_splits[best_index]
|
|||
|
|
|||
|
def split_by(self, item, original, splitter):
|
|||
|
if item.count(splitter) <= 2:
|
|||
|
return [[item.split(splitter), original.split(splitter)]]
|
|||
|
|
|||
|
item_all_split = item.split(splitter)
|
|||
|
original_all_split = original.split(splitter)
|
|||
|
all_possible_splits = [[item_all_split, original_all_split]]
|
|||
|
for i in range(2, 4):
|
|||
|
item_partially_split = []
|
|||
|
original_partially_split = []
|
|||
|
for j in range(0, len(item_all_split), i):
|
|||
|
item_join = splitter.join(item_all_split[j:j + i])
|
|||
|
original_join = splitter.join(original_all_split[j:j + i])
|
|||
|
item_partially_split.append(item_join)
|
|||
|
original_partially_split.append(original_join)
|
|||
|
all_possible_splits.append([item_partially_split, original_partially_split])
|
|||
|
return all_possible_splits
|
|||
|
|
|||
|
def split_if_not_parsed(self, item, original):
|
|||
|
splitters = [',', '،', '——', '—', '–', '.', ' ']
|
|||
|
possible_splits = []
|
|||
|
for splitter in splitters:
|
|||
|
if splitter in item and item.count(splitter) == original.count(splitter):
|
|||
|
possible_splits.extend(self.split_by(item, original, splitter))
|
|||
|
return possible_splits
|
|||
|
|
|||
|
def parse_item(self, parser, item, translated_item, parsed, need_relative_base):
|
|||
|
relative_base = None
|
|||
|
item = item.replace('ngày', '')
|
|||
|
item = item.replace('am', '')
|
|||
|
parsed_item = parser.get_date_data(item)
|
|||
|
is_relative = date_is_relative(translated_item)
|
|||
|
|
|||
|
if need_relative_base:
|
|||
|
item, relative_base = self.set_relative_base(item, parsed)
|
|||
|
|
|||
|
if relative_base:
|
|||
|
parser._settings.RELATIVE_BASE = relative_base
|
|||
|
parsed_item = parser.get_date_data(item)
|
|||
|
return parsed_item, is_relative
|
|||
|
|
|||
|
def parse_found_objects(self, parser, to_parse, original, translated, settings):
|
|||
|
parsed = []
|
|||
|
substrings = []
|
|||
|
need_relative_base = True
|
|||
|
if settings.RELATIVE_BASE:
|
|||
|
need_relative_base = False
|
|||
|
for i, item in enumerate(to_parse):
|
|||
|
if len(item) <= 2:
|
|||
|
continue
|
|||
|
|
|||
|
parsed_item, is_relative = self.parse_item(parser, item, translated[i], parsed, need_relative_base)
|
|||
|
if parsed_item['date_obj']:
|
|||
|
parsed.append((parsed_item, is_relative))
|
|||
|
substrings.append(original[i].strip(" .,:()[]-'"))
|
|||
|
continue
|
|||
|
|
|||
|
possible_splits = self.split_if_not_parsed(item, original[i])
|
|||
|
if not possible_splits:
|
|||
|
continue
|
|||
|
|
|||
|
possible_parsed = []
|
|||
|
possible_substrings = []
|
|||
|
for split_translated, split_original in possible_splits:
|
|||
|
current_parsed = []
|
|||
|
current_substrings = []
|
|||
|
if split_translated:
|
|||
|
for j, jtem in enumerate(split_translated):
|
|||
|
if len(jtem) <= 2:
|
|||
|
continue
|
|||
|
parsed_jtem, is_relative_jtem = self.parse_item(
|
|||
|
parser, jtem, split_translated[j], current_parsed, need_relative_base)
|
|||
|
current_parsed.append((parsed_jtem, is_relative_jtem))
|
|||
|
current_substrings.append(split_original[j].strip(' .,:()[]-'))
|
|||
|
possible_parsed.append(current_parsed)
|
|||
|
possible_substrings.append(current_substrings)
|
|||
|
parsed_best, substrings_best = self.choose_best_split(possible_parsed, possible_substrings)
|
|||
|
for k in range(len(parsed_best)):
|
|||
|
if parsed_best[k][0]['date_obj']:
|
|||
|
parsed.append(parsed_best[k])
|
|||
|
substrings.append(substrings_best[k])
|
|||
|
return parsed, substrings
|
|||
|
|
|||
|
def search_parse(self, shortname, text, settings):
|
|||
|
translated, original = self.search(shortname, text, settings)
|
|||
|
bad_translate_with_search = ['vi', 'hu'] # splitting done by spaces and some dictionary items contain spaces
|
|||
|
if shortname not in bad_translate_with_search:
|
|||
|
languages = ['en']
|
|||
|
to_parse = translated
|
|||
|
else:
|
|||
|
languages = [shortname]
|
|||
|
to_parse = original
|
|||
|
|
|||
|
parser = DateDataParser(languages=languages, settings=settings)
|
|||
|
parsed, substrings = self.parse_found_objects(parser=parser, to_parse=to_parse,
|
|||
|
original=original, translated=translated, settings=settings)
|
|||
|
parser._settings = Settings()
|
|||
|
return list(zip(substrings, [i[0]['date_obj'] for i in parsed]))
|
|||
|
|
|||
|
|
|||
|
class DateSearchWithDetection:
|
|||
|
"""
|
|||
|
Class which executes language detection of string in a natural language, translation of a given string,
|
|||
|
search of substrings which represent date and/or time and parsing of these substrings.
|
|||
|
|
|||
|
"""
|
|||
|
def __init__(self):
|
|||
|
self.loader = LocaleDataLoader()
|
|||
|
self.available_language_map = self.loader.get_locale_map()
|
|||
|
self.search = _ExactLanguageSearch(self.loader)
|
|||
|
|
|||
|
@apply_settings
|
|||
|
def detect_language(self, text, languages, settings=None, detect_languages_function=None):
|
|||
|
if detect_languages_function and not languages:
|
|||
|
detected_languages = detect_languages_function(
|
|||
|
text, confidence_threshold=settings.LANGUAGE_DETECTION_CONFIDENCE_THRESHOLD
|
|||
|
)
|
|||
|
detected_languages = map_languages(detected_languages) or settings.DEFAULT_LANGUAGES
|
|||
|
return detected_languages[0] if detected_languages else None
|
|||
|
|
|||
|
if isinstance(languages, (list, tuple, Set)):
|
|||
|
if all([language in self.available_language_map for language in languages]):
|
|||
|
languages = [self.available_language_map[language] for language in languages]
|
|||
|
else:
|
|||
|
unsupported_languages = set(languages) - set(self.available_language_map.keys())
|
|||
|
raise ValueError("Unknown language(s): %s" % ', '.join(map(repr, unsupported_languages)))
|
|||
|
elif languages is not None:
|
|||
|
raise TypeError("languages argument must be a list (%r given)" % type(languages))
|
|||
|
|
|||
|
if languages:
|
|||
|
self.language_detector = FullTextLanguageDetector(languages=languages)
|
|||
|
else:
|
|||
|
self.language_detector = FullTextLanguageDetector(list(self.available_language_map.values()))
|
|||
|
|
|||
|
detected_language = self.language_detector._best_language(text) or (
|
|||
|
settings.DEFAULT_LANGUAGES[0] if settings.DEFAULT_LANGUAGES else None
|
|||
|
)
|
|||
|
return detected_language
|
|||
|
|
|||
|
@apply_settings
|
|||
|
def search_dates(self, text, languages=None, settings=None, detect_languages_function=None):
|
|||
|
"""
|
|||
|
Find all substrings of the given string which represent date and/or time and parse them.
|
|||
|
|
|||
|
:param text:
|
|||
|
A string in a natural language which may contain date and/or time expressions.
|
|||
|
:type text: str
|
|||
|
|
|||
|
:param languages:
|
|||
|
A list of two letters language codes.e.g. ['en', 'es']. If languages are given, it will not attempt
|
|||
|
to detect the language.
|
|||
|
:type languages: list
|
|||
|
|
|||
|
:param settings:
|
|||
|
Configure customized behavior using settings defined in :mod:`dateparser.conf.Settings`.
|
|||
|
:type settings: dict
|
|||
|
|
|||
|
:param detect_languages_function:
|
|||
|
A function for language detection that takes as input a `text` and a `confidence_threshold`,
|
|||
|
returns a list of detected language codes.
|
|||
|
:type detect_languages_function: function
|
|||
|
|
|||
|
:return: a dict mapping keys to two letter language code and a list of tuples of pairs:
|
|||
|
substring representing date expressions and corresponding :mod:`datetime.datetime` object.
|
|||
|
For example:
|
|||
|
{'Language': 'en', 'Dates': [('on 4 October 1957', datetime.datetime(1957, 10, 4, 0, 0))]}
|
|||
|
If language of the string isn't recognised returns:
|
|||
|
{'Language': None, 'Dates': None}
|
|||
|
:raises: ValueError - Unknown Language
|
|||
|
"""
|
|||
|
|
|||
|
check_settings(settings)
|
|||
|
|
|||
|
language_shortname = self.detect_language(
|
|||
|
text=text, languages=languages, settings=settings, detect_languages_function=detect_languages_function
|
|||
|
)
|
|||
|
if not language_shortname:
|
|||
|
return {'Language': None, 'Dates': None}
|
|||
|
return {'Language': language_shortname, 'Dates': self.search.search_parse(language_shortname, text,
|
|||
|
settings=settings)}
|