From 34a22886e602f85702e527e603ec44dfb4941d7a Mon Sep 17 00:00:00 2001 From: Oleg Navolotsky Date: Wed, 24 Oct 2018 12:24:53 +0300 Subject: [PATCH 1/4] somemeh --- .gitignore | 21 ++++------ README.md | 9 ----- link_analysis/api_module.py | 76 ++++++++++++++++++++----------------- link_analysis/models.py | 2 +- 4 files changed, 51 insertions(+), 57 deletions(-) diff --git a/.gitignore b/.gitignore index 7778c8a..ec509e0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,16 +4,15 @@ link_analysis/my_funs.py run.cmd #Decision Files Decision files0/ -Decision Files/* -<<<<<<< HEAD -Decision Files0/ -env64/ -link_analysis/json_to_pickle_converter.py -link_analysis/my_funs.py -======= ->>>>>>> 2baad1f36ae87adc1badf8aaf9b08a48e9b3d127 +Decision Files/ *.pickle *.json + +# test graph +graph.json + + +.DS_Store # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -101,6 +100,7 @@ celerybeat-schedule .env .venv env/ +env64/ venv/ ENV/ env.bak/ @@ -122,9 +122,4 @@ venv.bak/ # VS code .vscode -# test graph -graph.json - - -.DS_Store diff --git a/README.md b/README.md index 3094952..437787b 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,5 @@ # Judist link analysis. Этот репозиторий посвящен компоненте анализа ссылок в юридических документах. Посетите [wiki](https://github.com/robot-lab/judyst-link-analysis/wiki) для получения большей информации. -<<<<<<< HEAD *** [Команда](https://github.com/robot-lab/judyst-main-web-service/wiki/Team-members) [Репозиторий проекта](https://github.com/robot-lab/judyst-main-web-service) -======= - - -*** -[Команда](https://github.com/robot-lab/judyst-main-web-service/wiki/Team-members) - -[Репозиторий проекта](https://github.com/robot-lab/judyst-main-web-service) ->>>>>>> 5bfc65508b0bc1f319fc4136ab659cc995b5a36b diff --git a/link_analysis/api_module.py b/link_analysis/api_module.py index 6ccf684..9b132b8 100644 --- a/link_analysis/api_module.py +++ b/link_analysis/api_module.py @@ -5,9 +5,9 @@ # other imports--------------------------------------------------------- import os.path -from datetime import date +import datetime -from dateutil import parser +import dateutil.parser # License: Apache Software License, BSD License (Dual License) # imports Core modules-------------------------------------------------- @@ -30,7 +30,7 @@ PICKLE_HEADERS_FILENAME) PATH_TO_JSON_GRAPH = 'graph.json' -MY_DEBUG = False +MY_DEBUG = True def collect_headers(pathToFileForSave, pagesNum=None): headersOld = ksrf.get_decision_headers(pagesNum) @@ -102,10 +102,10 @@ def process_period( draw graph and show it to user. ''' if isinstance(firstDateOfDocsForProcessing, str): - firstDateOfDocsForProcessing = parser.parse( + firstDateOfDocsForProcessing = dateutil.parser.parse( firstDateOfDocsForProcessing, dayfirst=True).date() if isinstance(lastDateOfDocsForProcessing, str): - lastDateOfDocsForProcessing = parser.parse( + lastDateOfDocsForProcessing = dateutil.parser.parse( lastDateOfDocsForProcessing, dayfirst=True).date() if (firstDateOfDocsForProcessing is not None and lastDateOfDocsForProcessing is not None and @@ -114,10 +114,10 @@ def process_period( "than the last date.") if isinstance(firstDateForNodes, str): - firstDateForNodes = parser.parse( + firstDateForNodes = dateutil.parser.parse( firstDateForNodes, dayfirst=True).date() if isinstance(lastDateForNodes, str): - lastDateForNodes = parser.parse( + lastDateForNodes = dateutil.parser.parse( lastDateForNodes, dayfirst=True).date() if (firstDateForNodes is not None and lastDateForNodes is not None and @@ -126,10 +126,10 @@ def process_period( "than the last date.") if isinstance(firstDateFrom, str): - firstDateFrom = parser.parse( + firstDateFrom = dateutil.parser.parse( firstDateFrom, dayfirst=True).date() if isinstance(lastDateFrom, str): - lastDateFrom = parser.parse( + lastDateFrom = dateutil.parser.parse( lastDateFrom, dayfirst=True).date() if (firstDateFrom is not None and lastDateFrom is not None and @@ -137,10 +137,10 @@ def process_period( raise ValueError("date error: The first date is later than the last date.") if isinstance(firstDateTo, str): - firstDateTo = parser.parse( + firstDateTo = dateutil.parser.parse( firstDateTo, dayfirst=True).date() if isinstance(lastDateTo, str): - lastDateTo = parser.parse( + lastDateTo = dateutil.parser.parse( lastDateTo, dayfirst=True).date() if (firstDateTo is not None and lastDateTo is not None and @@ -173,11 +173,13 @@ def process_period( if (rough_analysis.PATH_NONE_VALUE_KEY in roughLinksDict or rough_analysis.PATH_NOT_EXIST_KEY in roughLinksDict): raise ValueError('Some headers have no text') - links = final_analysis.get_clean_links(roughLinksDict, - decisionsHeaders)[0] - + + response = final_analysis.get_clean_links(roughLinksDict, + decisionsHeaders) + links, rejectedLinks = response[0], response[1] if MY_DEBUG: converters.save_pickle(links, 'allCleanLinks.pickle') + converters.save_pickle(rejectedLinks, 'allRejectedLinks.pickle') linkGraph = final_analysis.get_link_graph(links) if MY_DEBUG: converters.save_pickle(linkGraph, 'linkGraph.pickle') @@ -229,10 +231,10 @@ def start_process_with( raise ValueError("Unknown uid") if isinstance(firstDateForNodes, str): - firstDateForNodes = parser.parse( + firstDateForNodes = dateutil.parser.parse( firstDateForNodes, dayfirst=True).date() if isinstance(lastDateForNodes, str): - lastDateForNodes = parser.parse( + lastDateForNodes = dateutil.parser.parse( lastDateForNodes, dayfirst=True).date() if (firstDateForNodes is not None and lastDateForNodes is not None and @@ -240,10 +242,10 @@ def start_process_with( raise ValueError("date error: The first date is later than the last date.") if isinstance(firstDateFrom, str): - firstDateFrom = parser.parse( + firstDateFrom = dateutil.parser.parse( firstDateFrom, dayfirst=True).date() if isinstance(lastDateFrom, str): - lastDateFrom = parser.parse( + lastDateFrom = dateutil.parser.parse( lastDateFrom, dayfirst=True).date() if (firstDateFrom is not None and lastDateFrom is not None and @@ -251,10 +253,10 @@ def start_process_with( raise ValueError("date error: The first date is later than the last date.") if isinstance(firstDateTo, str): - firstDateTo = parser.parse( + firstDateTo = dateutil.parser.parse( firstDateTo, dayfirst=True).date() if isinstance(lastDateTo, str): - lastDateTo = parser.parse( + lastDateTo = dateutil.parser.parse( lastDateTo, dayfirst=True).date() if (firstDateTo is not None and lastDateTo is not None and @@ -317,7 +319,8 @@ def start_process_with( start_time = time.time() # process_period("18.06.1980", "18.07.2020", showPicture=False, # isNeedReloadHeaders=False, includeIsolatedNodes=True) - + # process_period("18.06.1980", "18.07.2020", showPicture=False, + # isNeedReloadHeaders=False, includeIsolatedNodes=False) # process_period( # firstDateOfDocsForProcessing='18.03.2013', # lastDateOfDocsForProcessing='14.08.2018', @@ -335,21 +338,26 @@ def start_process_with( # showPicture=True, isNeedReloadHeaders=False) # start_process_with(decisionID='КСРФ/1-П/2015', depth=3) + # load_and_visualize() - start_process_with( - decisionID='КСРФ/1-П/2015', depth=10, - firstDateForNodes='18.03.2014', lastDateForNodes='14.08.2018', - nodesIndegreeRange=(0, 25), nodesOutdegreeRange=(0, 25), - nodesTypes={'КСРФ/О', 'КСРФ/П'}, - includeIsolatedNodes=False, - firstDateFrom='18.03.2011', lastDateFrom='14.08.2019', - docTypesFrom={'КСРФ/О', 'КСРФ/П'}, - firstDateTo='18.03.2011', lastDateTo='14.08.2018', - docTypesTo={'КСРФ/О', 'КСРФ/П'}, - weightsRange=(1, 5), - graphOutputFilePath=PATH_TO_JSON_GRAPH, - showPicture=True, isNeedReloadHeaders=False) + # start_process_with( + # decisionID='КСРФ/1-П/2015', depth=10, + # firstDateForNodes='18.03.2014', lastDateForNodes='14.08.2018', + # nodesIndegreeRange=(0, 25), nodesOutdegreeRange=(0, 25), + # nodesTypes={'КСРФ/О', 'КСРФ/П'}, + # includeIsolatedNodes=False, + # firstDateFrom='18.03.2011', lastDateFrom='14.08.2019', + # docTypesFrom={'КСРФ/О', 'КСРФ/П'}, + # firstDateTo='18.03.2011', lastDateTo='14.08.2018', + # docTypesTo={'КСРФ/О', 'КСРФ/П'}, + # weightsRange=(1, 5), + # graphOutputFilePath=PATH_TO_JSON_GRAPH, + # showPicture=True, isNeedReloadHeaders=False) + + + h1=models.Header('key', 'type', 'title', releaseDate=datetime.date(1232, 1,3), + textSourceUrl='url') print(f"Headers collection spent {time.time()-start_time} seconds.") # get_only_unique_headers() input('press any key...') \ No newline at end of file diff --git a/link_analysis/models.py b/link_analysis/models.py index 2b5cd4e..7a9d424 100644 --- a/link_analysis/models.py +++ b/link_analysis/models.py @@ -99,7 +99,7 @@ class Header(DocumentHeader): def __init__(self, docID: str, docType: str, title: str, releaseDate: datetime.date, textSourceUrl: str, - textLocation: Optional[str]) -> None: + textLocation: Optional[str]=None) -> None: """ Constructor which uses superclass constructor passing it an arg docID. From 4eb882fa095bd0e440e96921e3f1a7a7d9f29b5b Mon Sep 17 00:00:00 2001 From: Oleg Navolotsky Date: Wed, 24 Oct 2018 12:50:10 +0300 Subject: [PATCH 2/4] merging --- link_analysis/api_module.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/link_analysis/api_module.py b/link_analysis/api_module.py index 9b132b8..82be7be 100644 --- a/link_analysis/api_module.py +++ b/link_analysis/api_module.py @@ -355,9 +355,5 @@ def start_process_with( # graphOutputFilePath=PATH_TO_JSON_GRAPH, # showPicture=True, isNeedReloadHeaders=False) - - h1=models.Header('key', 'type', 'title', releaseDate=datetime.date(1232, 1,3), - textSourceUrl='url') print(f"Headers collection spent {time.time()-start_time} seconds.") - # get_only_unique_headers() input('press any key...') \ No newline at end of file From 991c0653a3c7985591dfa205ae9112bcc82f3315 Mon Sep 17 00:00:00 2001 From: Oleg Navolotsky Date: Wed, 24 Oct 2018 20:47:33 +0300 Subject: [PATCH 3/4] added supertype, closes #87 --- .gitignore | 2 + dist/link_analysis-0.1-py3-none-any.whl | Bin 12712 -> 24483 bytes dist/link_analysis-0.1.tar.gz | Bin 13224 -> 41748 bytes link_analysis/api_module.py | 40 ++++++--- link_analysis/converters.py | 4 +- link_analysis/models.py | 108 +++++++++++++++++++----- link_analysis/rough_analysis.py | 40 +++++---- 7 files changed, 139 insertions(+), 55 deletions(-) diff --git a/.gitignore b/.gitignore index ec509e0..ea2b3ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ *.rar +ksrf_temp_folder/ +TestResults/ link_analysis/json_to_pickle_converter.py link_analysis/my_funs.py run.cmd diff --git a/dist/link_analysis-0.1-py3-none-any.whl b/dist/link_analysis-0.1-py3-none-any.whl index 6fc3a26537ed063877f44edd365da17a5c74abeb..3f116eada95625604ede58865af6ee1d572910d0 100644 GIT binary patch delta 18171 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Ð@EkTn4RRzqTt5E>t+@$ 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 aunlhPUqG-GHZ~Rj3xM&w8!*>ji2nmEu602G diff --git a/dist/link_analysis-0.1.tar.gz b/dist/link_analysis-0.1.tar.gz index 73932145b2804db4590ee58b63dfc531811c3e25..b8243b147d11314cc8afe917f98a719bd089cabd 100644 GIT binary patch literal 41748 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

TJTD5m~*523!7&G&O3lf6D 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 DHNm6^ literal 13224 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 Ku?U6(4)#9-M;8qM diff --git a/link_analysis/api_module.py b/link_analysis/api_module.py index 82be7be..d384b56 100644 --- a/link_analysis/api_module.py +++ b/link_analysis/api_module.py @@ -17,6 +17,7 @@ import visualizer import converters from web_crawler import ksrf +import web_crawler # methods--------------------------------------------------------------- @@ -59,7 +60,7 @@ def download_texts_for_headers(headers, folder=DECISIONS_FOLDER_NAME): (headers[key].text_location is None or not os.path.exists(headers[key].text_location))): oldFormatHeader = headers[key].convert_to_dict() - ksrf.download_decision_texts({key: oldFormatHeader}, folder) + ksrf.download_all_texts({key: oldFormatHeader}, folder) def load_graph(pathToGraph=PATH_TO_JSON_GRAPH): @@ -86,12 +87,15 @@ def load_and_visualize(pathTograph=PATH_TO_JSON_GRAPH): def process_period( firstDateOfDocsForProcessing=None, lastDateOfDocsForProcessing=None, + supertypesForProcessing=None, docTypesForProcessing=None, firstDateForNodes=None, lastDateForNodes=None, nodesIndegreeRange=None, nodesOutdegreeRange=None, nodesTypes=None, includeIsolatedNodes=True, firstDateFrom=None, lastDateFrom=None, docTypesFrom=None, + supertypesFrom=None, firstDateTo=None, lastDateTo=None, docTypesTo=None, + supertypesTo=None, weightsRange=None, graphOutputFilePath=PATH_TO_JSON_GRAPH, showPicture=True, isNeedReloadHeaders=False): @@ -149,12 +153,14 @@ def process_period( decisionsHeaders = {} if (isNeedReloadHeaders or not os.path.exists(PATH_TO_PICKLE_HEADERS)): - num = 3 # stub, del after web_crawler updating - decisionsHeaders = collect_headers(PATH_TO_PICKLE_HEADERS, num) + # num = 3 # stub, del after web_crawler updating + # decisionsHeaders = collect_headers(PATH_TO_PICKLE_HEADERS, num) + decisionsHeaders = collect_headers(PATH_TO_PICKLE_HEADERS) else: decisionsHeaders = converters.load_pickle(PATH_TO_PICKLE_HEADERS) hFilter = models.HeadersFilter( + supertypesForProcessing, docTypesForProcessing, firstDateOfDocsForProcessing, lastDateOfDocsForProcessing) usingHeaders = hFilter.get_filtered_headers(decisionsHeaders) @@ -178,24 +184,26 @@ def process_period( decisionsHeaders) links, rejectedLinks = response[0], response[1] if MY_DEBUG: - converters.save_pickle(links, 'allCleanLinks.pickle') - converters.save_pickle(rejectedLinks, 'allRejectedLinks.pickle') + converters.save_pickle(links, 'TestResults\\allCleanLinks.pickle') + converters.save_pickle(rejectedLinks, 'TestResults\\allRejectedLinks.pickle') linkGraph = final_analysis.get_link_graph(links) if MY_DEBUG: - converters.save_pickle(linkGraph, 'linkGraph.pickle') + converters.save_pickle(linkGraph, 'TestResults\\linkGraph.pickle') nFilter = models.GraphNodesFilter( nodesTypes, firstDateForNodes, lastDateForNodes, nodesIndegreeRange, nodesOutdegreeRange) hFromFilter = models.HeadersFilter( + supertypesFrom, docTypesFrom, firstDateFrom, lastDateFrom) hToFilter = models.HeadersFilter( + supertypesTo, docTypesTo, firstDateTo, lastDateTo) eFilter = models.GraphEdgesFilter(hFromFilter, hToFilter, weightsRange) subgraph = linkGraph.get_subgraph(nFilter, eFilter, includeIsolatedNodes) if MY_DEBUG: - converters.save_pickle(subgraph, 'subgraph.pickle') + converters.save_pickle(subgraph, 'TestResults\\subgraph.pickle') linkGraphLists = (subgraph.get_nodes_as_IDs_list(), subgraph.get_edges_as_list_of_tuples()) @@ -210,7 +218,9 @@ def start_process_with( firstDateForNodes=None, lastDateForNodes=None, nodesIndegreeRange=None, nodesOutdegreeRange=None, nodesTypes=None, includeIsolatedNodes=True, firstDateFrom=None, lastDateFrom=None, docTypesFrom=None, + supertypesFrom=None, firstDateTo=None, lastDateTo=None, docTypesTo=None, + supertypesTo=None, weightsRange=None, graphOutputFilePath=PATH_TO_JSON_GRAPH, showPicture=True, isNeedReloadHeaders=False, @@ -223,8 +233,9 @@ def start_process_with( raise "argument error: depth of the recursion must be large than 0." if isNeedReloadHeaders or not os.path.exists(PATH_TO_PICKLE_HEADERS): - num = 3 # stub, del after web_crawler updating - headers = collect_headers(PATH_TO_PICKLE_HEADERS, num) + # num = 3 # stub, del after web_crawler updating + # headers = collect_headers(PATH_TO_PICKLE_HEADERS, num) + headers = collect_headers(PATH_TO_PICKLE_HEADERS) else: headers = converters.load_pickle(PATH_TO_PICKLE_HEADERS) if (decisionID not in headers): @@ -294,9 +305,11 @@ def start_process_with( nodesTypes, firstDateForNodes, lastDateForNodes, nodesIndegreeRange, nodesOutdegreeRange) hFromFilter = models.HeadersFilter( + supertypesFrom, docTypesFrom, firstDateFrom, lastDateFrom) hToFilter = models.HeadersFilter( + supertypesTo, docTypesTo, firstDateTo, lastDateTo) eFilter = models.GraphEdgesFilter(hFromFilter, hToFilter, weightsRange) @@ -317,8 +330,8 @@ def start_process_with( if __name__ == "__main__": import time start_time = time.time() - # process_period("18.06.1980", "18.07.2020", showPicture=False, - # isNeedReloadHeaders=False, includeIsolatedNodes=True) + process_period("18.06.1980", "18.07.2020", showPicture=False, + isNeedReloadHeaders=False, includeIsolatedNodes=False) # process_period("18.06.1980", "18.07.2020", showPicture=False, # isNeedReloadHeaders=False, includeIsolatedNodes=False) # process_period( @@ -339,7 +352,7 @@ def start_process_with( # start_process_with(decisionID='КСРФ/1-П/2015', depth=3) - # load_and_visualize() + load_and_visualize() # start_process_with( # decisionID='КСРФ/1-П/2015', depth=10, @@ -354,6 +367,7 @@ def start_process_with( # weightsRange=(1, 5), # graphOutputFilePath=PATH_TO_JSON_GRAPH, # showPicture=True, isNeedReloadHeaders=False) - + # source = web_crawler.Crawler.get_data_source('LocalFileStorage') + # text=source.get_data('КСРФ/19-П/2014', web_crawler.DataType.DOCUMENT_TEXT) print(f"Headers collection spent {time.time()-start_time} seconds.") input('press any key...') \ No newline at end of file diff --git a/link_analysis/converters.py b/link_analysis/converters.py index c809f85..d1fe7ad 100644 --- a/link_analysis/converters.py +++ b/link_analysis/converters.py @@ -66,7 +66,7 @@ def save_json(jsonSerializableData: object, pathToFile: str) -> bool: dirname = os.path.dirname(pathToFile) if dirname: os.makedirs(dirname, exist_ok=True) - with open(pathToFile, 'w') as jsonFile: + with open(pathToFile, 'w', encoding='utf-8') as jsonFile: json.dump(jsonSerializableData, jsonFile) except OSError: return False @@ -75,7 +75,7 @@ def save_json(jsonSerializableData: object, pathToFile: str) -> bool: def load_json(pathToFile: str) -> Union[object, None]: try: - with open(pathToFile) as jsonFile: + with open(pathToFile, encoding='utf-8') as jsonFile: data = json.load(jsonFile) except OSError: return None diff --git a/link_analysis/models.py b/link_analysis/models.py index 7a9d424..3c128d2 100644 --- a/link_analysis/models.py +++ b/link_analysis/models.py @@ -79,6 +79,8 @@ class Header(DocumentHeader): :attribute doc_id: str. ID of document. + :attribute supertype: str. + Supertype of document. :attribute doc_type: str. Type of document. :attribute title: str. @@ -97,7 +99,7 @@ class Header(DocumentHeader): Called from superclass by iterface method with same name. """ - def __init__(self, docID: str, docType: str, title: str, + def __init__(self, docID: str, supertype: str, docType: str, title: str, releaseDate: datetime.date, textSourceUrl: str, textLocation: Optional[str]=None) -> None: @@ -106,6 +108,8 @@ def __init__(self, docID: str, docType: str, title: str, :param docID: str. ID of document. + :param supertype: str. + Supertype of document. :param docType: str. Type of document. :param title: str. @@ -118,6 +122,10 @@ def __init__(self, docID: str, docType: str, title: str, Location of text document. """ super().__init__(docID) + if isinstance(docType, str): + self.supertype = supertype + else: + raise TypeError(f"'supertype' must be instance of {supertype}") if isinstance(docType, str): self.doc_type = docType else: @@ -140,7 +148,11 @@ def __init__(self, docID: str, docType: str, title: str, raise TypeError(f"'textLocation' must be instance of {str}") def __eq__(self, other): + if not isinstance(other, type(self)): + raise TypeError(f"Compared objects must be of the same type:" + f"{type(self)} or {type(other)}") return (super().__eq__(self) and + self.supertype == other.supertype and self.doc_type == other.doc_type and self.title == other.title and self.release_date == other.release_date and @@ -161,6 +173,7 @@ def convert_to_dict(self): Dict object that stores values of attributes of instance. """ dictFormatHeader = { + 'supertype': self.supertype, 'doc_type': self.doc_type, 'title': self.title, 'release_date': self.release_date.strftime('%d.%m.%Y'), @@ -192,6 +205,7 @@ def convert_from_dict(key: str, oldFormatHeader: dict): try: docID = key + supertype = oldFormatHeader['supertype'] docType = oldFormatHeader['doc_type'] title = oldFormatHeader['title'] releaseDate = dateutil.parser.parse(oldFormatHeader['release_date'], @@ -202,9 +216,10 @@ def convert_from_dict(key: str, oldFormatHeader: dict): else: textLocation = None except KeyError: - raise KeyError("'doc_type', 'title', 'release_date', 'text_source_url' is required, " + raise KeyError("'doc_type', 'supertype', 'title', 'release_date', " + "'text_source_url' is required, " "only 'path to file' is optional") - return Header(docID, docType, title, releaseDate, textSourceUrl, textLocation) + return Header(docID, supertype, docType, title, releaseDate, textSourceUrl, textLocation) class DuplicateHeader(DocumentHeader): @@ -229,7 +244,7 @@ class DuplicateHeader(DocumentHeader): Called from superclass by iterface method with same name. """ - def __init__(self, docID, docType=None, title=None, releaseDate=None, + def __init__(self, docID, supertype=None, docType=None, title=None, releaseDate=None, textSourceUrl=None, textLocation=None): """ Constructor with optinal arguments. You must specify either @@ -239,6 +254,8 @@ def __init__(self, docID, docType=None, title=None, releaseDate=None, :param docID: str. Common ID of duplicated documents. + :param supertype: str. + Supertype of first duplicated document that be added at list. :param docType: str. Type of first duplicated document that be added at list. :param title: str. @@ -254,6 +271,10 @@ def __init__(self, docID, docType=None, title=None, releaseDate=None, super().__init__(docID) else: raise TypeError(f"'docID' must be instance of {str}") + if isinstance(supertype, str) or supertype is None: + self.supertype = supertype + else: + raise TypeError(f"'supertype' must be instance of {str}") if isinstance(docType, str) or docType is None: self.doc_type = docType else: @@ -275,18 +296,23 @@ def __init__(self, docID, docType=None, title=None, releaseDate=None, else: raise TypeError(f"'textLocation' must be instance of {str}") - if (docType is None and title is None and releaseDate is None and - textSourceUrl is None and textLocation is None): + if (supertype is None and docType is None and title is None and + releaseDate is None and textSourceUrl is None and + textLocation is None): self.header_list = [] - elif (docType is not None and title is not None and - releaseDate is not None and textSourceUrl is not None): - self.header_list = [Header(docID, docType, title, + elif (supertype is not None and docType is not None and + title is not None and releaseDate is not None and + textSourceUrl is not None): + self.header_list = [Header(docID, supertype, docType, title, releaseDate, textSourceUrl, textLocation)] else: raise ValueError("You must specify either argument 'docID' only or" " all arguments except optional 'textLocation'") def __eq__(self, other): + if not isinstance(other, type(self)): + raise TypeError(f"Compared objects must be of the same type:" + f"{type(self)} or {type(other)}") return (super().__eq__(other) and (collections.Counter(self.header_list) == collections.Counter(other.header_list))) @@ -297,12 +323,14 @@ def __ne__(self, other): def __hash__(self): return super().__hash__() - def append(self, docType, title, releaseDate, textSourceUrl, + def append(self, supertype, docType, title, releaseDate, textSourceUrl, textLocation=None): """ Append instance of class Header that contains data about duplicated document at self.header_list. + :param supertype: str. + Supertype of document that be added at list. :param docType: str. Type of document that be added at list. :param title: str. @@ -314,6 +342,10 @@ def append(self, docType, title, releaseDate, textSourceUrl, :param textLocation: str, optional (default=None). Text location of document that be added at list. """ + if isinstance(supertype, str) or supertype is None: + self.supertype = supertype + else: + raise TypeError(f"'supertype' must be instance of {str}") if isinstance(docType, str) or docType is None: self.doc_type = docType else: @@ -335,7 +367,7 @@ def append(self, docType, title, releaseDate, textSourceUrl, else: raise TypeError(f"'textLocation' must be instance of {str}") - h = Header(self.doc_id, docType, title, releaseDate, textSourceUrl, + h = Header(self.doc_id, supertype, docType, title, releaseDate, textSourceUrl, textLocation) if h not in self.header_list: self.header_list.append(h) @@ -350,6 +382,7 @@ def convert_to_dict(self): dhList = [] for dupHeader in self.header_list: dh = { + 'supertype': dupHeader.supertype, 'doc_type': dupHeader.doc_type, 'title': dupHeader.title, 'release_date': dupHeader.release_date.strftime('%d.%m.%Y'), @@ -383,6 +416,7 @@ def convert_from_dict(key: str, oldFormatHeader: dict): duplicateHeader = DuplicateHeader(docID) try: for dh in oldFormatHeader[1]: + supertype = dh['supertype'] docType = dh['doc_type'] title = dh['title'] releaseDate = dateutil.parser.parse(dh['release_date'], @@ -392,11 +426,13 @@ def convert_from_dict(key: str, oldFormatHeader: dict): textLocation = dh['text_location'] else: textLocation = None - duplicateHeader.append(docType, title, releaseDate, + duplicateHeader.append(supertype, docType, title, releaseDate, textSourceUrl, textLocation) except KeyError: - raise KeyError("'doc_type', 'title', 'release_date', 'text_source_url' is required, " - "only 'text_location' is optional") + raise KeyError( + "'supertype', 'doc_type', 'title', 'release_date', " + "'text_source_url' is required, only 'text_location' " + "is optional") return duplicateHeader @@ -412,6 +448,9 @@ def __init__(self, headerFrom): self.header_from = headerFrom def __eq__(self, other): + if not isinstance(other, type(self)): + raise TypeError(f"Compared objects must be of the same type:" + f"{type(self)} or {type(other)}") return self.header_from == other.header_from def __ne__(self, other): @@ -436,6 +475,9 @@ def __init__(self, headerFrom, body, context, position): self.position = position def __eq__(self, other): + if not isinstance(other, type(self)): + raise TypeError(f"Compared objects must be of the same type:" + f"{type(self)} or {type(other)}") return (super().__eq__(other) and self.context == other.context and self.body == other.body and @@ -472,6 +514,9 @@ def __init__(self, headerFrom, headerTo, citationsNumber, self.positions_and_contexts = list(positionsAndContexts) def __eq__(self, other): + if not isinstance(other, type(self)): + raise TypeError(f"Compared objects must be of the same type:" + f"{type(self)} or {type(other)}") return (super().__eq__(other) and self.header_to == other.header_to and self.citations_number == other.citations_number and @@ -504,9 +549,12 @@ class HeadersFilter(): Arguments contains conditions for which headers will be selected.\n firstDate and lastDate: instances of datetime.date """ - def __init__(self, docTypes=None, firstDate=None, + def __init__(self, supertypes=None, docTypes=None, firstDate=None, lastDate=None): - + if hasattr(supertypes, '__iter__'): + self.supertypes = set(supertypes) + else: + self.supertypes = None if hasattr(docTypes, '__iter__'): self.doc_types = set(docTypes) else: @@ -528,7 +576,11 @@ def __init__(self, docTypes=None, firstDate=None, "of datetime.date") def __eq__(self, other): - return (self.doc_types == other.doc_types and + if not isinstance(other, type(self)): + raise TypeError(f"Compared objects must be of the same type:" + f"{type(self)} or {type(other)}") + return (self.supertypes == other.supertypes and + self.doc_types == other.doc_types and self.first_date == other.first_date and self.last_date == other.last_date) @@ -536,13 +588,16 @@ def __ne__(self, other): return not self.__eq__(other) def __hash__(self): - return hash(tuple([hash(tuple(self.doc_types)), + return hash(tuple([hash(tuple(self.supertypes)), + hash(tuple(self.doc_types)), hash(self.first_date), hash(self.last_date)])) def check_header(self, header): - if ((self.doc_types is None or - header.doc_type in self.doc_types) and + if ((self.supertypes is None or + header.supertype in self.supertypes) and + (self.doc_types is None or + header.doc_type in self.doc_types) and self.first_date <= header.release_date <= self.last_date): return True else: @@ -564,14 +619,17 @@ class GraphNodesFilter(HeadersFilter): indegreeRange and outdegreeRange: tuples that implements own line segment [int, int] """ - def __init__(self, docTypes=None, firstDate=None, + def __init__(self, supertype=None, docTypes=None, firstDate=None, lastDate=None, indegreeRange=None, outdegreeRange=None): - super().__init__(docTypes, firstDate, lastDate) + super().__init__(supertype, docTypes, firstDate, lastDate) self.indegree_range = indegreeRange self.outdegree_range = outdegreeRange def __eq__(self, other): + if not isinstance(other, type(self)): + raise TypeError(f"Compared objects must be of the same type:" + f"{type(self)} or {type(other)}") return (super().__eq__(other) and self.indegree_range == other.indegree_range and self.outdegree_range == other.outdegree_range) @@ -598,6 +656,9 @@ def __init__(self, headersFilterFrom=None, headerFilterTo=None, self.weights_range = weightsRange def __eq__(self, other): + if not isinstance(other, type(self)): + raise TypeError(f"Compared objects must be of the same type:" + f"{type(self)} or {type(other)}") return (self.headers_filter_from == other.headers_filter_from and self.headers_filter_to == other.headers_filter_to and self.weights_range == other.weights_range) @@ -643,6 +704,9 @@ def __init__(self): self.edges = set() def __eq__(self, other): + if not isinstance(other, type(self)): + raise TypeError(f"Compared objects must be of the same type:" + f"{type(self)} or {type(other)}") return (self.nodes == other.nodes and self.edges == other.edges) diff --git a/link_analysis/rough_analysis.py b/link_analysis/rough_analysis.py index 50705c6..1a2f940 100644 --- a/link_analysis/rough_analysis.py +++ b/link_analysis/rough_analysis.py @@ -102,21 +102,25 @@ def get_rough_links_for_multiple_docs( if (__name__ == '__main__'): from datetime import date - h1 = Header('2028-О/2018', 'КСРФ/О', 'some title1', date(2018, 7, 17), - 'http://doc.ksrf.ru/decision/KSRFDecision353855.pdf', - r'Decision files\КСРФ_2028-О_2018.txt') - h2 = Header('36-П/2018', 'КСРФ/П', 'some title2', date(2018, 10, 15), - 'http://doc.ksrf.ru/decision/KSRFDecision357397.pdf') - h3 = Header('33-П/2018', 'КСРФ/П', 'some title3', date(2018, 7, 18), - 'http://doc.ksrf.ru/decision/KSRFDecision343519.pdf', - r'Decision files\КСРФ_33-П_2018.txt') - h4 = Header('30-П/2018', 'КСРФ/П', 'some title4', date(2018, 7, 10), - 'http://doc.ksrf.ru/decision/KSRFDecision342302.pdf', - r'path that not exist') - h5 = Header('841-О/2018', 'КСРФ/О', 'some title5', date(2018, 4, 12), - 'http://doc.ksrf.ru/decision/KSRFDecision332975.pdf', - r'Decision files\КСРФ_841-О_2018.txt') - headers = {'2028-О/2018': h1, '36-П/2018': h2, '33-П/2018': h3, - '30-П/2018': h4, '841-О/2018': h5} - roughLinks = get_rough_links_for_multiple_docs(headers) - input('press any key...') + # h1 = Header('2028-О/2018', 'КСРФ/О', 'some title1', date(2018, 7, 17), + # 'http://doc.ksrf.ru/decision/KSRFDecision353855.pdf', + # r'Decision files\КСРФ_2028-О_2018.txt') + # h2 = Header('36-П/2018', 'КСРФ/П', 'some title2', date(2018, 10, 15), + # 'http://doc.ksrf.ru/decision/KSRFDecision357397.pdf') + # h3 = Header('33-П/2018', 'КСРФ/П', 'some title3', date(2018, 7, 18), + # 'http://doc.ksrf.ru/decision/KSRFDecision343519.pdf', + # r'Decision files\КСРФ_33-П_2018.txt') + # h4 = Header('30-П/2018', 'КСРФ/П', 'some title4', date(2018, 7, 10), + # 'http://doc.ksrf.ru/decision/KSRFDecision342302.pdf', + # r'path that not exist') + # h5 = Header('841-О/2018', 'КСРФ/О', 'some title5', date(2018, 4, 12), + # 'http://doc.ksrf.ru/decision/KSRFDecision332975.pdf', + # r'Decision files\КСРФ_841-О_2018.txt') + # headers = {'2028-О/2018': h1, '36-П/2018': h2, '33-П/2018': h3, + # '30-П/2018': h4, '841-О/2018': h5} + # roughLinks = get_rough_links_for_multiple_docs(headers) + # input('press any key...') + + h = Header('test5', 'ксрф', 'А', 'title', date(1991, 3, 2), 'url', r'C:\VS Code Projects\test5.txt') + r=get_rough_links(h) + print('s') \ No newline at end of file From 15d47890604175ec3d4a22b3596dc278dcbc932e Mon Sep 17 00:00:00 2001 From: Oleg Navolotsky Date: Thu, 25 Oct 2018 18:50:06 +0300 Subject: [PATCH 4/4] Added supertype closes #87 Deleted class DuplicateHeader closes Added saving context positions and link positions closes #91 Added type checks in classes in link_analysis.models closes #89 Updated converting CleanLink in JSON closes #74 --- .gitignore | 1 + link_analysis/api_module.py | 11 +- link_analysis/converters.py | 2 +- link_analysis/final_analysis.py | 13 +- link_analysis/models.py | 414 +++++++++++--------------------- link_analysis/rough_analysis.py | 75 +++--- 6 files changed, 196 insertions(+), 320 deletions(-) diff --git a/.gitignore b/.gitignore index ea2b3ea..c2b9743 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ ksrf_temp_folder/ TestResults/ link_analysis/json_to_pickle_converter.py link_analysis/my_funs.py +link_analysis/archive.py run.cmd #Decision Files Decision files0/ diff --git a/link_analysis/api_module.py b/link_analysis/api_module.py index d384b56..ca8d8d7 100644 --- a/link_analysis/api_module.py +++ b/link_analysis/api_module.py @@ -330,8 +330,8 @@ def start_process_with( if __name__ == "__main__": import time start_time = time.time() - process_period("18.06.1980", "18.07.2020", showPicture=False, - isNeedReloadHeaders=False, includeIsolatedNodes=False) + # process_period("18.06.1980", "18.07.2020", showPicture=False, + # isNeedReloadHeaders=False, includeIsolatedNodes=False) # process_period("18.06.1980", "18.07.2020", showPicture=False, # isNeedReloadHeaders=False, includeIsolatedNodes=False) # process_period( @@ -352,7 +352,7 @@ def start_process_with( # start_process_with(decisionID='КСРФ/1-П/2015', depth=3) - load_and_visualize() + # load_and_visualize() # start_process_with( # decisionID='КСРФ/1-П/2015', depth=10, @@ -369,5 +369,10 @@ def start_process_with( # showPicture=True, isNeedReloadHeaders=False) # source = web_crawler.Crawler.get_data_source('LocalFileStorage') # text=source.get_data('КСРФ/19-П/2014', web_crawler.DataType.DOCUMENT_TEXT) + + # process_period("18.09.2018", "18.07.2020", showPicture=True, + # isNeedReloadHeaders=False, includeIsolatedNodes=True) + import my_funs + my_funs.saving_all_clean_links() print(f"Headers collection spent {time.time()-start_time} seconds.") input('press any key...') \ No newline at end of file diff --git a/link_analysis/converters.py b/link_analysis/converters.py index d1fe7ad..1a5d49f 100644 --- a/link_analysis/converters.py +++ b/link_analysis/converters.py @@ -3,7 +3,7 @@ import os from typing import Dict, Iterable, TypeVar, Type, List, Union, Any -from models import Header, DuplicateHeader, DocumentHeader +from models import Header, DocumentHeader from final_analysis import CleanLink # Don't forget to add to this place new classes where implemented diff --git a/link_analysis/final_analysis.py b/link_analysis/final_analysis.py index 698b032..b52d272 100644 --- a/link_analysis/final_analysis.py +++ b/link_analysis/final_analysis.py @@ -1,5 +1,5 @@ import re -from models import Header, DuplicateHeader, CleanLink +from models import Header, CleanLink, Positions from models import LinkGraph from rough_analysis import RoughLink from typing import Dict, Tuple, List, Union @@ -10,7 +10,7 @@ def get_clean_links( collectedLinks: Dict[Header, List[RoughLink]], - courtSiteContent: Dict[str, Union[Header, DuplicateHeader]], + courtSiteContent: Dict[str, Header], courtPrefix: str='КСРФ/') -> Tuple[Dict[Header, List[CleanLink]], Dict[Header, List[RoughLink]]]: ''' @@ -35,17 +35,10 @@ def get_clean_links( gottenID = (courtPrefix + number[0].upper() + '/' + years.pop()) if gottenID in courtSiteContent: - try: - if isinstance(courtSiteContent[gottenID], - DuplicateHeader): - raise TypeError("It links on duplicating " - "document") - except TypeError: - break eggs = True years.clear() headerTo = courtSiteContent[gottenID] - positionAndContext = (link.position, link.context) + positionAndContext = link.positions cleanLink = None for cl in checkedLinks[headerFrom]: if cl.header_to == headerTo: diff --git a/link_analysis/models.py b/link_analysis/models.py index 3c128d2..c213702 100644 --- a/link_analysis/models.py +++ b/link_analysis/models.py @@ -1,6 +1,6 @@ import datetime import collections -from typing import Type, Optional, Union +from typing import Type, Optional, Union, Dict import dateutil.parser # License: Apache Software Licenseid, BSD License (Dual License) @@ -46,29 +46,25 @@ def __hash__(self) -> int: @staticmethod def convert_from_dict(key: str, - oldFormatHeader: dict):# -> Type[DocumentHeader]: + oldFormatHeader: Dict):# -> Type[DocumentHeader]: """ Convert dict object to instance of subclass of class DocumentHeader. :param key: str. Key which related with oldFormatHeader. - :param oldFormatHeader: {dict}. + :param oldFormatHeader: dict. Dict object that stores data about document. :return: DocumentHeader. Instance of one of subclasses (Header or DuplicateHeader). """ - + if 'not unique' in oldFormatHeader: + raise TypeError("'class DuplicateHeader' is not supported anymore.") if not isinstance(key, str): raise TypeError(f"'key' must be instance of {str}") - if (not isinstance(oldFormatHeader, dict) and not isinstance(oldFormatHeader, tuple) and - not isinstance(oldFormatHeader, list)): - raise TypeError(f"'oldFormatHeader' must be instance of {dict} or {tuple} or {list}") - - if 'not unique' in oldFormatHeader: - return DuplicateHeader.convert_from_dict(key, oldFormatHeader) - else: - return Header.convert_from_dict(key, oldFormatHeader) + if not isinstance(oldFormatHeader, dict) : + raise TypeError(f"'oldFormatHeader' must be instance of {dict}") + return Header.convert_from_dict(key, oldFormatHeader) class Header(DocumentHeader): @@ -201,7 +197,7 @@ def convert_from_dict(key: str, oldFormatHeader: dict): if not isinstance(key, str): raise TypeError(f"'key' mus be instance of {str}") if not isinstance(oldFormatHeader, dict): - raise TypeError("'oldFormatHeader' must be instance of 'dict'") + raise TypeError(f"'oldFormatHeader' must be instance of {dict}'") try: docID = key @@ -222,275 +218,101 @@ def convert_from_dict(key: str, oldFormatHeader: dict): return Header(docID, supertype, docType, title, releaseDate, textSourceUrl, textLocation) -class DuplicateHeader(DocumentHeader): - - """ - Subclass of DocumentHeader. Implements storage of data - about document whose identifier is not unique. - - :attribute doc_id: str. - Common ID of duplicated documents. - :attribute header_list: list. - List with instances of class Header. - Any of them stores data about one of the duplicated documents. - - :method append: instancemethod. - Append instance of class Header that contains data - about duplicated document at self.header_list. - :method convert_to_dict: instancemethod. - Convert instance to dict object. - :method convert_from_dict: staticmethod. - Convert dict object to instance of own class. - Called from superclass by iterface method with same name. - """ - - def __init__(self, docID, supertype=None, docType=None, title=None, releaseDate=None, - textSourceUrl=None, textLocation=None): +class Link: + def __init__(self, headerFrom): """ - Constructor with optinal arguments. You must specify either - argument 'docID' only to create empty list that ready to append - new elements or all arguments except optional 'textLocation' to create - list with first element. - - :param docID: str. - Common ID of duplicated documents. - :param supertype: str. - Supertype of first duplicated document that be added at list. - :param docType: str. - Type of first duplicated document that be added at list. - :param title: str. - Title of first duplicated document that be added at list. - :param releaseDate: datetime.date. - Release date of first duplicated document that be added at list. - :param textSourceUrl: str. - URL of text source of first duplicated document that be added at list. - :param textLocation: str, optional (default=None). - Text location of first duplicated document that be added at list. + :param headerFrom: class Header + Citing document """ - if isinstance(docID, str): - super().__init__(docID) - else: - raise TypeError(f"'docID' must be instance of {str}") - if isinstance(supertype, str) or supertype is None: - self.supertype = supertype - else: - raise TypeError(f"'supertype' must be instance of {str}") - if isinstance(docType, str) or docType is None: - self.doc_type = docType - else: - raise TypeError(f"'docType' must be instance of {str}") - if isinstance(title, str) or title is None: - self.title = title - else: - raise TypeError(f"'title' must be instance of {str}") - if isinstance(releaseDate, datetime.date) or releaseDate is None: - self.release_date = releaseDate - else: - raise TypeError(f"'release_date' must be instance of {datetime.date}") - if isinstance(textSourceUrl, str) or textSourceUrl is None: - self.text_source_url = textSourceUrl - else: - raise TypeError(f"'textSourceUrl' must be instance of {str}") - if isinstance(textLocation, str) or textLocation is None: - self.text_location = textLocation - else: - raise TypeError(f"'textLocation' must be instance of {str}") - - if (supertype is None and docType is None and title is None and - releaseDate is None and textSourceUrl is None and - textLocation is None): - self.header_list = [] - elif (supertype is not None and docType is not None and - title is not None and releaseDate is not None and - textSourceUrl is not None): - self.header_list = [Header(docID, supertype, docType, title, - releaseDate, textSourceUrl, textLocation)] - else: - raise ValueError("You must specify either argument 'docID' only or" - " all arguments except optional 'textLocation'") + if not isinstance(headerFrom, Header): + raise TypeError(f"'headerFrom' must be instance of {Header}") + self.header_from = headerFrom def __eq__(self, other): if not isinstance(other, type(self)): raise TypeError(f"Compared objects must be of the same type:" f"{type(self)} or {type(other)}") - return (super().__eq__(other) and - (collections.Counter(self.header_list) == - collections.Counter(other.header_list))) + return self.header_from == other.header_from def __ne__(self, other): return not self.__eq__(other) def __hash__(self): - return super().__hash__() + return hash(self.header_from) - def append(self, supertype, docType, title, releaseDate, textSourceUrl, - textLocation=None): - """ - Append instance of class Header that contains data - about duplicated document at self.header_list. +class Positions: - :param supertype: str. - Supertype of document that be added at list. - :param docType: str. - Type of document that be added at list. - :param title: str. - Title of document that be added at list. - :param releaseDate: datetime.date. - Release date of document that be added at list. - :param textSourceUrl: str. - URL of text source of document that be added at list. - :param textLocation: str, optional (default=None). - Text location of document that be added at list. - """ - if isinstance(supertype, str) or supertype is None: - self.supertype = supertype - else: - raise TypeError(f"'supertype' must be instance of {str}") - if isinstance(docType, str) or docType is None: - self.doc_type = docType + def __init__(self, contextStartPos, contextEndPos, linkStartPos, linkEndPos): + if isinstance(contextStartPos, int): + self.context_start = contextStartPos else: - raise TypeError(f"'docType' must be instance of {str}") - if isinstance(title, str) or title is None: - self.title = title + raise TypeError(f"'contextStartPos' must be {int}") + if isinstance(contextEndPos, int): + self.context_end = contextEndPos else: - raise TypeError(f"'title' must be instance of {str}") - if isinstance(releaseDate, datetime.date) or releaseDate is None: - self.release_date = releaseDate + raise TypeError(f"'contextEndPos' must be {int}") + if isinstance(linkStartPos, int): + self.link_start = linkStartPos else: - raise TypeError(f"'releaseDate' must be instance of {datetime.date}") - if isinstance(textSourceUrl, str) or textSourceUrl is None: - self.text_source_url = textSourceUrl + raise TypeError(f"'linkStartPos' must be {int}") + if isinstance(linkEndPos, int): + self.link_end = linkEndPos else: - raise TypeError(f"'textSourceUrl' must be instance of {str}") - if isinstance(textLocation, str) or textLocation is None: - self.text_location = textLocation - else: - raise TypeError(f"'textLocation' must be instance of {str}") - - h = Header(self.doc_id, supertype, docType, title, releaseDate, textSourceUrl, - textLocation) - if h not in self.header_list: - self.header_list.append(h) - - def convert_to_dict(self): - """ - Convert instance to dict object that stores all values of attributes of instance. - - :return: dict. - Dict object that stores values of attributes of instance. - """ - dhList = [] - for dupHeader in self.header_list: - dh = { - 'supertype': dupHeader.supertype, - 'doc_type': dupHeader.doc_type, - 'title': dupHeader.title, - 'release_date': dupHeader.release_date.strftime('%d.%m.%Y'), - 'text_source_url': dupHeader.text_source_url - } - if dupHeader.text_location is not None: - dh['text_location'] = dupHeader.text_location - dhList.append(dh) - return ('not unique', dhList) - - @staticmethod - def convert_from_dict(key: str, oldFormatHeader: dict): - """ - Convert dict object to instance of own class. - Called from superclass by iterface method with same name. - - :param key: str. - Key which related with oldFormatHeader. - :param oldFormatHeader: dict. - Dict object that stores data about document. - - :return: DuplicateHeader. - Instance of own class. - """ - if not isinstance(key, str): - raise TypeError(f"'key' mus be instance of {str}") - if (not isinstance(oldFormatHeader, dict) and not isinstance(oldFormatHeader, tuple) and - not isinstance(oldFormatHeader, list)): - raise TypeError(f"'oldFormatHeader' must be instance of {dict} or {tuple} or {list}") - docID = key - duplicateHeader = DuplicateHeader(docID) - try: - for dh in oldFormatHeader[1]: - supertype = dh['supertype'] - docType = dh['doc_type'] - title = dh['title'] - releaseDate = dateutil.parser.parse(dh['release_date'], - dayfirst=True).date() - textSourceUrl = dh['text_source_url'] - if 'text_location' in dh: - textLocation = dh['text_location'] - else: - textLocation = None - duplicateHeader.append(supertype, docType, title, releaseDate, - textSourceUrl, textLocation) - except KeyError: - raise KeyError( - "'supertype', 'doc_type', 'title', 'release_date', " - "'text_source_url' is required, only 'text_location' " - "is optional") - return duplicateHeader - - -class Link: - def __init__(self, headerFrom): - """ - :param headerFrom: class Header - Citing document - """ - if not isinstance(headerFrom, Header): - raise TypeError("Variable 'headerFrom' is not instance " - "of class Header") - self.header_from = headerFrom + raise TypeError(f"'linkEndPos' must be {int}") def __eq__(self, other): if not isinstance(other, type(self)): raise TypeError(f"Compared objects must be of the same type:" f"{type(self)} or {type(other)}") - return self.header_from == other.header_from + return (self.context_start == other.context_start and + self.context_end == other.context_end and + self.link_start == other.link_start and + self.link_end == other.link_end) def __ne__(self, other): return not self.__eq__(other) - + def __hash__(self): - return hash(self.header_from) - + return hash(tuple(hash(self.context_start), + hash(self.context_end), + hash(self.link_start), + hash(self.link_end) + )) class RoughLink(Link): - def __init__(self, headerFrom, body, context, position): + def __init__(self, headerFrom: Header, body: str, positions: Positions): """ :param headerFrom: class Header Citing document """ if not isinstance(headerFrom, Header): - raise TypeError("Variable 'headerFrom' is not instance " - "of class Header") + raise TypeError(f"'headerFrom' must be instance of {Header}") super().__init__(headerFrom) - self.body = body - self.context = context - self.position = position + if isinstance(body, str): + self.body = body + else: + raise TypeError(f"'body' must be instance of {str}") + if isinstance(positions, Positions): + self.positions = positions + else: + raise TypeError(f"'positions' must be instance of {Positions}") def __eq__(self, other): if not isinstance(other, type(self)): raise TypeError(f"Compared objects must be of the same type:" f"{type(self)} or {type(other)}") return (super().__eq__(other) and - self.context == other.context and self.body == other.body and - self.position == other.position) + self.positions == other.positions) def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash(tuple([super().__hash__(), - hash(self.context), hash(self.body), - hash(self.position)])) + hash(self.positions)])) + class CleanLink(Link): @@ -499,19 +321,31 @@ class member: positionsAndContexts: list of tuple(int, str) where int variable (position) is start position of str variable (contex) in text """ + def __init__(self, headerFrom, headerTo, citationsNumber, - positionsAndContexts): + positionsList): """ positionsAndContexts: tuple or list of tuples, " or set of tuples(int, str) """ super().__init__(headerFrom) - self.header_to = headerTo - self.citations_number = citationsNumber - if isinstance(positionsAndContexts, tuple): - self.positions_and_contexts = [positionsAndContexts] + if isinstance(headerTo, Header): + self.header_to = headerTo + else: + raise TypeError(f"'headerTo' must be instance of {Header}") + + if isinstance(citationsNumber, int): + self.citations_number = citationsNumber + else: + raise TypeError(f"'citationsNumber' must be instance of {int}") + if isinstance(positionsList, Positions): + self.positions_list = [positionsList] + elif (isinstance(positionsList, list) or + isinstance(positionsList, set) or + isinstance(positionsList, tuple)): + self.positions_list = list(positionsList) else: - self.positions_and_contexts = list(positionsAndContexts) + raise TypeError(f"'positionsList' must be instance of {list} or {tuple} or {set}") def __eq__(self, other): if not isinstance(other, type(self)): @@ -520,8 +354,8 @@ def __eq__(self, other): return (super().__eq__(other) and self.header_to == other.header_to and self.citations_number == other.citations_number and - (collections.Counter(self.positions_and_contexts) == - collections.Counter(other.positions_and_contexts))) + (collections.Counter(self.positions_list) == + collections.Counter(other.positions_list))) def __ne__(self, other): return not self.__eq__(other) @@ -530,17 +364,15 @@ def __hash__(self): return hash(tuple([super().__hash__(), hash(self.header_to)])) def append(self, positionAndContext: tuple): - self.positions_and_contexts.append(positionAndContext) + self.positions_list.append(positionAndContext) def convert_to_dict(self): cleanLinkDict = { 'doc_id_from': self.header_from.doc_id, - 'doc_id_to': self.header_to.doc_id, - 'to_doc_title': self.header_to.title, - 'citations_number': self.citations_number, - 'contexts_list': [pac[1] for pac in self.positions_and_contexts], - 'positions_list': [pac[0] for pac in self.positions_and_contexts] + 'doc_id_to': self.header_to.doc_id } + positionsDictList = [pos.__dict__ for pos in self.positions_list] + cleanLinkDict['positions_list'] = positionsDictList return cleanLinkDict @@ -553,27 +385,36 @@ def __init__(self, supertypes=None, docTypes=None, firstDate=None, lastDate=None): if hasattr(supertypes, '__iter__'): self.supertypes = set(supertypes) + for st in supertypes: + if not isinstance(st, str): + raise TypeError(f"any element from 'supertypes' must be instance of {str}") + elif supertypes is None: + self.supertypes = supertypes else: - self.supertypes = None + raise TypeError(f"'supertypes' must be iterable structure: {list}, {set}, {tuple}") + if hasattr(docTypes, '__iter__'): self.doc_types = set(docTypes) + for st in docTypes: + if not isinstance(st, str): + raise TypeError(f"any element from 'docTypes' must be instance of {str}") + elif docTypes is None: + self.doc_types = docTypes else: - self.doc_types = None + raise TypeError(f"'docTypes' must be iterable structure: {list}, {set}, {tuple}") if firstDate is None: self.first_date = datetime.date.min elif isinstance(firstDate, datetime.date): self.first_date = firstDate else: - raise TypeError("Variable 'firstDate' is not instance " - "of datetime.date") + raise TypeError(f"'firstDate' must be instance of {datetime.date}") if lastDate is None: self.last_date = datetime.date.max elif isinstance(lastDate, datetime.date): self.last_date = lastDate else: - raise TypeError("Variable 'lastDate' is not instance " - "of datetime.date") + raise TypeError(f"'lastDate' must be instance of {datetime.date}") def __eq__(self, other): if not isinstance(other, type(self)): @@ -594,6 +435,8 @@ def __hash__(self): hash(self.last_date)])) def check_header(self, header): + if not isinstance(header, Header): + raise TypeError(f"'header' must be instance of {Header}") if ((self.supertypes is None or header.supertype in self.supertypes) and (self.doc_types is None or @@ -603,7 +446,7 @@ def check_header(self, header): else: return False - def get_filtered_headers(self, headersDict): + def get_filtered_headers(self, headersDict: Dict[str, Header]) -> Dict[str, Header]: resultDict = {} for key in headersDict: if (isinstance(headersDict[key], Header) and @@ -623,8 +466,19 @@ def __init__(self, supertype=None, docTypes=None, firstDate=None, lastDate=None, indegreeRange=None, outdegreeRange=None): super().__init__(supertype, docTypes, firstDate, lastDate) - self.indegree_range = indegreeRange - self.outdegree_range = outdegreeRange + if (isinstance(indegreeRange, tuple) or isinstance(indegreeRange, list)): + self.indegree_range = tuple(indegreeRange) + elif indegreeRange is None: + self.indegree_range = indegreeRange + else: + raise TypeError(f"'indegreeRange' must be instance of {tuple} or {list}") + if (isinstance(outdegreeRange, tuple) or isinstance(outdegreeRange, list)): + self.outdegree_range = tuple(outdegreeRange) + elif outdegreeRange is None: + self.outdegree_range = outdegreeRange + else: + raise TypeError(f"'outdegreeRange' must be instance of {tuple} or {list}") + def __eq__(self, other): if not isinstance(other, type(self)): @@ -648,12 +502,25 @@ class GraphEdgesFilter(): Arguments contains conditions for which edges will be selected.\n weightsRange: tuple that implements line segment [int, int] """ - def __init__(self, headersFilterFrom=None, headerFilterTo=None, + def __init__(self, headersFilterFrom=None, headersFilterTo=None, weightsRange=None): + if (isinstance(headersFilterFrom, HeadersFilter) or + headersFilterFrom is None): + self.headers_filter_from = headersFilterFrom + else: + raise TypeError(f"'headersFilterFrom' must be instance of {HeadersFilter}") + if (isinstance(headersFilterTo, HeadersFilter) or + headersFilterTo is None): + self.headers_filter_to = headersFilterTo + else: + raise TypeError(f"'headersFilterTo' must be instance of {HeadersFilter}") + if (isinstance(weightsRange, tuple) or isinstance(weightsRange, list)): + self.weights_range = tuple(weightsRange) + elif weightsRange is None: + self.weights_range = weightsRange + else: + raise TypeError(f"'weightsRange' must be instance of {tuple} or {list}") - self.headers_filter_from = headersFilterFrom - self.headers_filter_to = headerFilterTo - self.weights_range = weightsRange def __eq__(self, other): if not isinstance(other, type(self)): @@ -673,6 +540,9 @@ def __hash__(self): def check_edge(self, edge: CleanLink): """edge: class CleanLink""" + if not isinstance(edge, CleanLink): + raise TypeError(f"'edge' must be instance of {CleanLink}") + if ((self.headers_filter_from is None or self.headers_filter_from.check_header(edge.header_from) ) and @@ -690,6 +560,8 @@ def get_filtered_edges(self, edges): edges: list or set of instances of class CleanLink\n returns set of instances of class CleanLink """ + if not (isinstance(edges, set) or isinstance(edges, list) or isinstance(edges, tuple)): + raise TypeError(f"'edge' must be of instance of {set} or {list} or {tuple}") result = {edge for edge in edges if self.check_edge(edge)} return result @@ -726,14 +598,14 @@ def __hash__(self): def add_node(self, node): if not isinstance(node, Header): - raise TypeError("Variable 'node' is not instance " - "of class Header") + raise TypeError(f"'node is not instance " + "of {Header}") self.nodes.add(node) def add_edge(self, edge): if not isinstance(edge, CleanLink): - raise TypeError("Variable 'edge' is not instance " - "of class CleanLink") + raise TypeError(f"'edge' is not instance " + "of {CleanLink}") self.edges.add(edge) def get_all_nodes_degrees(self): @@ -749,14 +621,14 @@ def get_all_nodes_degrees(self): def get_subgraph(self, nodesFilter=None, edgesFilter=None, includeIsolatedNodes=True): - if (nodesFilter is None and edgesFilter is None): - return self + if not isinstance(includeIsolatedNodes, bool): + raise TypeError("'includeIsolatedNodes' must be instance of {bool}") subgraph = LinkGraph() # filters nodes if nodesFilter is not None: if not isinstance(nodesFilter, GraphNodesFilter): - raise TypeError("Variable 'nodesFilter' is not instance " + raise TypeError(f"Variable 'nodesFilter' is not instance " "of class GraphNodesFilter") if (nodesFilter.indegree_range is not None or nodesFilter.outdegree_range is not None): @@ -783,7 +655,7 @@ def get_subgraph(self, nodesFilter=None, edgesFilter=None, # filters edges if edgesFilter is not None: if not isinstance(edgesFilter, GraphEdgesFilter): - raise TypeError("Variable 'edgesFilter' is not instance " + raise TypeError(f"Variable 'edgesFilter' is not instance " "of class GraphEdgesFilter") # If nodes are filtered, we must check the edges @@ -840,12 +712,6 @@ class IterableLinkGraph(LinkGraph): # stub # "https://goto.ru") # h4 = Header("456-О-О/2018", "КСРФ/О-О", "Заголовк", date, # "https://goto.ru") - h5 = DuplicateHeader("456-О-О/2018", "КСРФ/О-О", "Заголовк", date, - "https://goto.ru") - h6 = DuplicateHeader("426-О-О/2018", "КСРФ/О-О", "Заголовк", date, - "https://goto.ru") - print(hash(h5)) - h5 == h6 # h5.append("КСРФ/О-О", "Заголовк", datetime.date(1990, 1, 2), # "https://goto.ru") # h6 = DuplicateHeader("456-О-О/2018", "КСРФ/О-О", "Заголовк", date, diff --git a/link_analysis/rough_analysis.py b/link_analysis/rough_analysis.py index 1a2f940..f696a38 100644 --- a/link_analysis/rough_analysis.py +++ b/link_analysis/rough_analysis.py @@ -1,45 +1,43 @@ import re -from typing import Dict, List, Union -from models import Header, RoughLink, DuplicateHeader +from typing import Dict, List, Union, Type +from models import Header, RoughLink, Positions # link pattern main part lpMP = (r".*?\sот[\s\d]+?(?:(?:января|февраля|марта|апреля|мая|июня|июля|" r"августа|сентября|октября|ноября|декабря)+?[\s\d]+?года|\d{2}\." - r"\d{2}\.\d{4})[\s\d]+?(№|N)[\s\d]+?[-\w/]*.*?") + r"\d{2}\.\d{4})[\s\d]+?(?:№|N)[\s\d]+?[-\w/]*.*?") # link pattern prefix #1 -lpPRF1 = r"(?<=\.\s)\s*?[А-Я]" +lpPRF1 = r"(?<=\.\s)\s*?[А-ЯA-Z]" # link pattern postfix #1 -lpPSF1 = r"(?=\.\s[А-Я])" +lpPSF1 = r"(?=\.\s[А-ЯA-Z])" # link pattern prefix #2 -lpPRF2 = r"(?<=^)\s*?[А-Яа-я]" +lpPRF2 = r"(?<=^)\s*?[А-ЯA-Zа-яa-z]" # link pattern postfix #2 lpPSF2 = r"(?=\.$)" -linkPattern = re.compile( - f"(?:{lpPRF1+lpMP+lpPSF1}|{lpPRF1+lpMP+lpPSF2}|{lpPRF2+lpMP+lpPSF1}|" - f"{lpPRF2+lpMP+lpPSF2})", re.VERBOSE) +linkPattern = re.compile(f"""(?:{lpPRF1+lpMP+lpPSF1}|{lpPRF1+lpMP+lpPSF2}| + {lpPRF2+lpMP+lpPSF1}|{lpPRF2+lpMP+lpPSF2})""", re.VERBOSE) # pattern for removing of redundant leading sentences -reductionPattern = re.compile(r"(?:[А-Я].*[^А-Я]\.\s*(?=[А-Я])|^[А-Яа-я]" - r".*[^А-Я]\.\s*(?=[А-Я]))") +reductionPattern = re.compile(r"(?:[А-ЯA-Z].*[^А-ЯA-Z]\.\s*(?=[А-ЯA-Z])|^[А-ЯA-Zа-яa-z]" + r".*[^А-ЯA-Z]\.\s*(?=[А-ЯA-Z]))") # same part of two regular expressions below -samePart = (r"т[\s\d]+?(?:(?:января|февраля|марта|апреля|мая|июня|июля|" - r"августа|сентября|октября|ноября|декабря)+?[\s\d]+?года|\d{2}" - r"\.\d{2}\.\d{4})(?=\s)") -splitPattern = re.compile( - f"(?i)о(?={samePart})") -datePattern = re.compile( - f"(?i){samePart}") +splitPattern = re.compile(r"""(?i)о(?=т[\s\d]+?(?:(?:января|февраля|марта|апреля|мая|июня|июля| + августа|сентября|октября|ноября|декабря)+?[\s\d]+?года|\d{2} + \.\d{2}\.\d{4})[\s\d]+?(?:№|N))""") +datePattern = re.compile(r"""(?i)т[\s\d]+?(?:(?:января|февраля|марта|апреля|мая|июня|июля| + августа|сентября|октября|ноября|декабря)+?[\s\d]+?года|\d{2} + \.\d{2}\.\d{4})(?=\s)""") numberPattern = re.compile(r'(?:№|N)[\s\d]+[-\w/]*') opinionPattern = re.compile(r'(?i)мнение\s+судьи\s+конституционного') -def get_rough_links(header: Header) -> List[RoughLink]: +def get_rough_links(header: Header) -> Union[List[RoughLink], Type[TypeError], Type[FileNotFoundError]]: """ :param header: instance of class models.Header """ try: - with open(header.text_location, 'r', encoding="utf-8") as file: + with open(header.text_location, 'r', encoding="utf-8") as file: # debug file reading will be deleted soon text = file.read() except TypeError: return TypeError @@ -53,18 +51,31 @@ def get_rough_links(header: Header) -> List[RoughLink]: matchObjects = linkPattern.finditer(text) for match in matchObjects: linksForSplit = match[0] - context = reductionPattern.sub('', linksForSplit) + '.' - position = match.start(0) + len(splitPattern.split(linksForSplit, - maxsplit=1)[0]) + 1 + reduct = reductionPattern.search(linksForSplit) + if reduct is not None: + reductCorrection = reduct.end() + #context = linksForSplit.replace(reduct[0], '') + '.' + else: + reductCorrection = 0 + #context = linksForSplit + linkCorrection = len(splitPattern.split(linksForSplit, + maxsplit=1)[0]) + contextStartPos = match.start(0) + reductCorrection + contextEndPos = match.end(0) + 1 + linkStartPos = match.start(0) + linkCorrection + splitedLinksForDifferentYears = splitPattern.split(linksForSplit)[1:] for oneYearLinks in splitedLinksForDifferentYears: date = datePattern.search(oneYearLinks)[0] - numbers = numberPattern.findall(oneYearLinks) - for number in numbers: - gottenRoughLink = 'о' + date + ' ' + number.upper() - roughLinks.append(RoughLink(header, gottenRoughLink, context, - position)) - position += len(oneYearLinks) + 1 + matchNumbers = list(numberPattern.finditer(oneYearLinks)) + + linkEndPos = linkStartPos + matchNumbers[-1].end(0) + 1 + for number in matchNumbers: + gottenRoughLink = 'о' + date + ' ' + number[0].upper() + roughLinks.append(RoughLink(header, gottenRoughLink, + Positions(contextStartPos, contextEndPos, + linkStartPos, linkEndPos))) + linkStartPos += len(oneYearLinks) + 1 return roughLinks @@ -73,7 +84,7 @@ def get_rough_links(header: Header) -> List[RoughLink]: def get_rough_links_for_multiple_docs( - headers: Dict[str, Union[Header, DuplicateHeader]]) -> Dict[Header, List[RoughLink]]: + headers: Dict[str, Header]) -> Dict[Header, List[RoughLink]]: """ :param header: dict of instances of class models.Header return dict with list of instances of class RoughLink @@ -83,8 +94,8 @@ def get_rough_links_for_multiple_docs( """ result = {} # type: Dict[Header, List[RoughLink]] for decisionID in headers: - if isinstance(headers[decisionID], DuplicateHeader): - continue + if not isinstance(headers[decisionID], Header): + raise TypeError(f"Any element of 'headers' must be instance of {Header}") maybeRoughLinks = get_rough_links(headers[decisionID]) if maybeRoughLinks is TypeError: if PATH_NONE_VALUE_KEY not in result: