diff --git a/README.md b/README.md index 8bb785c..c9483cd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # py-phias Python application that can operate with FIAS (Russian Address Object DB) -Простое приложение для работы с БД ФИАС, написано для Python 2.7, использует БД PostgreSQL +Простое приложение для работы с БД ФИАС, написано для Python 3.4+, использует БД PostgreSQL ## Содержание - [Возможности](#Возможности) - [Установка](#Установка) @@ -26,7 +26,7 @@ Python application that can operate with FIAS (Russian Address Object DB) ### Зависимости -_Внимание_! Только Python 2.7+ (на 3+ не тестировал), только PostgreSQL, только Sphinx. MySQL/MariaDB, ElasticSearch/Solr +_Внимание_! Только Python 3 (для 2.7 пока есть отдельная ветка), только PostgreSQL, только Sphinx. MySQL/MariaDB, ElasticSearch/Solr не поддерживаются и, скорее всего, не будут. Для работы приложения необходимо достаточное кол-во RAM (1Gb+) и ~5.5Gb места на диске @@ -38,7 +38,7 @@ _Внимание_! Только Python 2.7+ (на 3+ не тестировал) Предварительно обязательно установить и настроить: -1. Python 2.7.x, pip +1. Python 3, pip Для Windows качаем - ставим, для Debian: ``` sudo apt-get install python-setuptools @@ -83,12 +83,15 @@ _Внимание_! Только Python 2.7+ (на 3+ не тестировал) 4. Web-сервер с поддержкой WSGI, любой, по Вашему желанию. ### Windows -1. Установить lxml, скачав [отсюда](https://pypi.python.org/pypi/lxml/3.5.0). +1. Установить lxml, скачав whl [отсюда](http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml) и сделав +`pip install yourmodulename.whl`. +2. Есть некоторые проблемы с установкой и работой psycopg2 (Windows 10, VS 2015), если у Вас они присутствуют - качаем +[сборку для Windows](http://www.stickpeople.com/projects/python/win-psycopg/) 2. Установить unrar.exe (можно установить WinRar целиком). -3. Установить sphinxapi последней версии (либо взять из директории Sphinx): +3. Установить sphinxapi с поддержкой синтаксиса Python3: ``` - python -m pip install https://github.com/Romamo/sphinxapi/zipball/master + pip install https://github.com/jar3b/sphinx-py3-api/zipball/master ``` 4. Установить приложение, скачав релиз `https://github.com/jar3b/py-phias/archive/v0.0.2.zip`, распакуйте его в удобное Вам место и запустите оттуда `python -m pip install -r requirements.txt` @@ -162,4 +165,28 @@ _Внимание_! Только Python 2.7+ (на 3+ не тестировал) - если необходимо, добавьте `searchd --config /usr/local/sphinx/etc/sphinx.conf` в `/etc/rc.local` для автостарта 5. Настроим WSGI server, я использую nginx + passenger. Конфиг для passenger - [passenger_wsgi.py](passenger_wsgi.py), конфиг для nginx - [py-phias.conf](https://gist.github.com/jar3b/f8f5d351e0ea8ae2ed8e). Вы можете -использовать любое приемлемое сочетание. \ No newline at end of file +использовать любое приемлемое сочетание. + +## Api + +- `/normalize/` - актуализирует AOID или AOGUID, на выходе выдает + + `{"aoid": "1d6185b5-25a6-4fe8-9208-e7ddd281926a"}`, где aoid - актуальный AOID. +- `/find/` и `/find//strong`- полнотекстовый поиск по названию адресного объекта. `` - строка поиска. +Если указан параметр `strong`, то поиск будет выдавать меньше результатов, но они будут точнее. Если же флаг не +указан, но будет выдано 10 наиболее релевантных результатов. + + На выходе будет массив: + ``` + [ + { + "cort": 0, + "text": "обл Псковская, р-н Порховский, д Гречушанка", + "ratio": 1537, + "aoid": "1d6185b5-25a6-4fe8-9208-e7ddd281926a" + }, + ... (up to 10) + ] + ``` + ,где cort - количество несовпавших слов, text - полное название адресного объекта, ratio - рейтинг, aoid - + актуальный AOID. \ No newline at end of file diff --git a/aore/__init__.py b/aore/__init__.py index eeecfb6..fd908b7 100644 --- a/aore/__init__.py +++ b/aore/__init__.py @@ -1,7 +1,5 @@ import os import sys -reload(sys) cwd_dir = os.getcwd() sys.path.append(cwd_dir) -sys.setdefaultencoding("utf-8") diff --git a/aore/config/common.py b/aore/config/common.py index 10a304e..772e786 100644 --- a/aore/config/common.py +++ b/aore/config/common.py @@ -3,6 +3,7 @@ class BasicConfig: logging = False + debug_print = False logfile = "" def __init__(self): diff --git a/aore/miscutils/fysearch.py b/aore/miscutils/fysearch.py index 367cb47..1c2d618 100644 --- a/aore/miscutils/fysearch.py +++ b/aore/miscutils/fysearch.py @@ -13,7 +13,7 @@ def violet_ratio(pattern, candidate): for i in range(len(arr_pattern) - 1, -1, -1): max_j = -1 max_ratio = -1 - allowed_nums = range(len(arr_candidate) - 1, -1, -1) + allowed_nums = list(range(len(arr_candidate) - 1, -1, -1)) for j in allowed_nums: ratio = Levenshtein.ratio(arr_pattern[i], arr_candidate[j]) @@ -24,7 +24,7 @@ def violet_ratio(pattern, candidate): result.append(max_j*abs(max_ratio)) if max_j > -1: - allowed_nums.remove(max_j) + del allowed_nums[max_j] del arr_candidate[max_j] return sum(result) - len(arr_candidate) diff --git a/aore/miscutils/sphinx.py b/aore/miscutils/sphinx.py index 22c1542..6987127 100644 --- a/aore/miscutils/sphinx.py +++ b/aore/miscutils/sphinx.py @@ -22,11 +22,11 @@ class SphinxHelper: os.makedirs(Folders.temp) # оздаем 3 папки для Сфинкса - if not os.path.exists(SphinxConfig.var_dir+ '/run'): + if not os.path.exists(SphinxConfig.var_dir + '/run'): os.makedirs(SphinxConfig.var_dir + '/run') - if not os.path.exists(SphinxConfig.var_dir+ '/log'): + if not os.path.exists(SphinxConfig.var_dir + '/log'): os.makedirs(SphinxConfig.var_dir + '/log') - if not os.path.exists(SphinxConfig.var_dir+ '/data'): + if not os.path.exists(SphinxConfig.var_dir + '/data'): os.makedirs(SphinxConfig.var_dir + '/data') def configure_indexer(self, indexer_binary, config_filename): @@ -55,7 +55,7 @@ class SphinxHelper: logging.info("All indexes were created.") # remove temp files - for fname, fpath in self.files.iteritems(): + for fname, fpath in self.files.items(): try: os.remove(fpath) except: @@ -84,10 +84,10 @@ class SphinxHelper: def __dbexport_sugg_dict(self): logging.info("Place suggestion dict to DB %s...", self.files['dict.txt']) - dict_dat_fname = os.path.abspath(Folders.temp + "/suggdict.csv") + fname = os.path.abspath(Folders.temp + "/suggdict.csv") csv_counter = 0 - with open(self.files['dict.txt'], "r") as dict_file, open(dict_dat_fname, "w") as exit_file: + with open(self.files['dict.txt'], "r") as dict_file, open(fname, "w") as exit_file: line = None while line != '': nodes = [] @@ -111,9 +111,11 @@ class SphinxHelper: except: pass - self.aodp.bulk_csv(AoXmlTableEntry.OperationType.update, "AOTRIG", csv_counter, dict_dat_fname) + self.aodp.bulk_csv(AoXmlTableEntry.OperationType.update, "AOTRIG", csv_counter, fname) logging.info("Done.") + return fname + def __create_ao_index_config(self): fname = os.path.abspath(Folders.temp + "/addrobj.conf") logging.info("Creating config %s", fname) @@ -156,7 +158,7 @@ class SphinxHelper: sphinx_var_path=SphinxConfig.var_dir) f = open(out_filename, "w") - for fname, fpath in self.files.iteritems(): + for fname, fpath in self.files.items(): if ".conf" in fname: with open(fpath, "r") as conff: for line in conff: diff --git a/aore/phias.py b/aore/phias.py index d82b1b5..8c4ca9e 100644 --- a/aore/phias.py +++ b/aore/phias.py @@ -5,7 +5,7 @@ import logging from bottle import response from aore.search.fiasfactory import FiasFactory -from miscutils.bottlecl import BottleCL +from .miscutils.bottlecl import BottleCL class App(BottleCL): diff --git a/aore/search/fiasfactory.py b/aore/search/fiasfactory.py index defefaa..f09becb 100644 --- a/aore/search/fiasfactory.py +++ b/aore/search/fiasfactory.py @@ -1,16 +1,16 @@ # -*- coding: utf-8 -*- import logging import re -import urllib +import traceback +import urllib.parse from uuid import UUID import psycopg2 -import traceback from bottle import template from aore.config import DatabaseConfig, BasicConfig from aore.dbutils.dbimpl import DBImpl -from search import SphinxSearch +from .search import SphinxSearch class FiasFactory: @@ -38,10 +38,10 @@ class FiasFactory: if rule == "boolean": assert isinstance(param, bool), "Invalid parameter type" if rule == "uuid": - assert (isinstance(param, str) or isinstance(param, unicode)) and self.__check_uuid( + assert isinstance(param, str) and self.__check_uuid( param), "Invalid parameter value" if rule == "text": - assert isinstance(param, str) or isinstance(param, unicode), "Invalid parameter type" + assert isinstance(param, str), "Invalid parameter type" assert len(param) > 3, "Text too short" pattern = re.compile(r"[A-za-zА-Яа-я \-,.#№]+") assert pattern.match(param), "Invalid parameter value" @@ -52,15 +52,17 @@ class FiasFactory: def find(self, text, strong=False): try: - text = urllib.unquote(text).decode('utf8') + text = urllib.parse.unquote(str(text)) self.__check_param(text, "text") self.__check_param(strong, "boolean") results = self.searcher.find(text, strong) - except Exception, err: + except Exception as err: if BasicConfig.logging: - logging.error(traceback.format_exc(err)) - return dict(error=err.args[0]) + logging.error(traceback.format_exc()) + if BasicConfig.debug_print: + traceback.print_exc() + return dict(error=str(err)) return results @@ -73,10 +75,12 @@ class FiasFactory: rows = self.db.get_rows(sql_query, True) assert len(rows), "Record with this AOID not found in DB" - except Exception, err: + except Exception as err: if BasicConfig.logging: - logging.error(traceback.format_exc(err)) - return dict(error=err.args[0]) + logging.error(traceback.format_exc()) + if BasicConfig.debug_print: + traceback.print_exc() + return dict(error=str(err)) if len(rows) == 0: return [] @@ -94,10 +98,12 @@ class FiasFactory: sql_query = self.expand_templ.replace("//aoid", normalized_id) rows = self.db.get_rows(sql_query, True) - except Exception, err: + except Exception as err: if BasicConfig.logging: - logging.error(traceback.format_exc(err)) - return dict(error=err.args[0]) + logging.error(traceback.format_exc()) + if BasicConfig.debug_print: + traceback.print_exc() + return dict(error=str(err)) return rows @@ -111,9 +117,11 @@ class FiasFactory: rows = self.db.get_rows(sql_query, True) assert len(rows), "Record with this AOID not found in DB" - except Exception, err: + except Exception as err: if BasicConfig.logging: - logging.error(traceback.format_exc(err)) - return dict(error=err.args[0]) + logging.error(traceback.format_exc()) + if BasicConfig.debug_print: + traceback.print_exc() + return dict(error=str(err)) return rows diff --git a/aore/search/search.py b/aore/search/search.py index 0b879ca..30e87d5 100644 --- a/aore/search/search.py +++ b/aore/search/search.py @@ -11,8 +11,8 @@ from aore.config import SphinxConfig from aore.miscutils.exceptions import FiasException from aore.miscutils.fysearch import violet_ratio from aore.miscutils.trigram import trigram -from wordentry import WordEntry -from wordvariation import VariationType +from .wordentry import WordEntry +from .wordvariation import VariationType class SphinxSearch: @@ -62,7 +62,7 @@ class SphinxSearch: self.client_show.SetSortMode(sphinxapi.SPH_SORT_EXTENDED, "krank DESC") def __get_suggest(self, word, rating_limit, count): - word_len = str(len(word) / 2) + word_len = len(word) trigrammed_word = '"{}"/1'.format(trigram(word)) self.__configure(SphinxConfig.index_sugg, word_len) @@ -95,7 +95,7 @@ class SphinxSearch: return outlist # Получает список объектов (слово) - def __get_word_entries(self, words, strong): + def __get_word_entries(self, words): we_list = [] for word in words: if word != '': @@ -111,14 +111,14 @@ class SphinxSearch: def find(self, text, strong): def split_phrase(phrase): - phrase = unicode(phrase).lower() + phrase = phrase.lower() return re.split(r"[ ,:.#$]+", phrase) # сплитим текст на слова words = split_phrase(text) # получаем список объектов (слов) - word_entries = self.__get_word_entries(words, strong) + word_entries = self.__get_word_entries(words) word_count = len(word_entries) # проверяем, есть ли вообще что-либо в списке объектов слов (или же все убрали как частое) @@ -169,7 +169,7 @@ class SphinxSearch: parsed_ids.append(match['attrs']['aoid']) results.append( dict(aoid=match['attrs']['aoid'], - text=unicode(match['attrs']['fullname']), + text=str(match['attrs']['fullname']), ratio=match['attrs']['krank'], cort=i)) diff --git a/aore/search/wordentry.py b/aore/search/wordentry.py index 99a3b07..5ff0077 100644 --- a/aore/search/wordentry.py +++ b/aore/search/wordentry.py @@ -5,6 +5,10 @@ from aore.config import SphinxConfig from aore.search.wordvariation import WordVariation, VariationType +def cleanup_string(word): + return word.replace('-', '').replace('@', '').replace('#', '') + + class WordEntry: # Варианты распеределния для слов с первыми двумя символами, где: # 0 - не найдено, 1 - найдено одно, x - найдено много (>1) @@ -51,14 +55,14 @@ class WordEntry: def __init__(self, db, word): self.db = db - self.bare_word = str(word) - self.word = self.__cleanify(self.bare_word) - self.word_len = len(unicode(self.word)) + self.bare_word = word + self.word = cleanup_string(self.bare_word) + self.word_len = len(self.word) self.parameters = dict(IS_FREQ=False, SOCR_WORD=None) self.ranks = self.__init_ranks() # Заполняем параметры слова - for mt_name, mt_values in self.match_types.iteritems(): + for mt_name, mt_values in self.match_types.items(): self.__dict__[mt_name] = False for mt_value in mt_values: self.__dict__[mt_name] = self.__dict__[mt_name] or re.search(mt_value, self.ranks) @@ -72,9 +76,6 @@ class WordEntry: self.MT_LAST_STAR = False self.MT_AS_IS = True - def __cleanify(self, word): - return word.replace('-', '').replace('@', '') - def variations_generator(self, strong, suggestion_func): default_var_type = VariationType.normal # Если слово встречается часто, ставим у всех вариантов тип VariationType.freq @@ -115,8 +116,9 @@ class WordEntry: "UNION ALL SELECT COUNT(*), NULL FROM \"AOTRIG\" WHERE word='{}' " \ "UNION ALL SELECT COUNT(*), MAX(scname) FROM \"SOCRBASE\" WHERE socrname ILIKE '{}'" \ "UNION ALL SELECT COUNT(*), NULL FROM \"SOCRBASE\" WHERE scname ILIKE '{}'" \ - "UNION ALL SELECT frequency, NULL FROM \"AOTRIG\" WHERE word='{}';".format( - self.word, self.word_len, self.word, self.bare_word, self.bare_word, self.word) + "UNION ALL SELECT frequency, NULL FROM \"AOTRIG\" WHERE word='{}';".format(self.word, self.word_len, + self.word, self.bare_word, + self.bare_word, self.word) result = self.db.get_rows(sql_qry) diff --git a/aore/updater/aorar.py b/aore/updater/aorar.py index 05c1d36..8ea80e6 100644 --- a/aore/updater/aorar.py +++ b/aore/updater/aorar.py @@ -9,7 +9,7 @@ import requests from aore.config import Folders, UnrarConfig from aore.miscutils.exceptions import FiasException -from aoxmltableentry import AoXmlTableEntry +from .aoxmltableentry import AoXmlTableEntry class AoRar: diff --git a/aore/updater/soapreceiver.py b/aore/updater/soapreceiver.py index e43c9ef..42c7735 100644 --- a/aore/updater/soapreceiver.py +++ b/aore/updater/soapreceiver.py @@ -2,6 +2,7 @@ from pysimplesoap.client import SoapClient + class SoapReceiver: def __init__(self): self.client = SoapClient( diff --git a/aore/updater/updater.py b/aore/updater/updater.py index 5babd7f..8bc9f28 100644 --- a/aore/updater/updater.py +++ b/aore/updater/updater.py @@ -62,9 +62,9 @@ class Updater: mode = None while not mode: try: - mode = int(raw_input('Enter FIAS update version (3 digit):')) + mode = int(input('Enter FIAS update version (3 digit):')) except ValueError: - print "Not a valid fias version, try again." + print("Not a valid fias version, try again.") return mode diff --git a/aore/updater/xmlparser.py b/aore/updater/xmlparser.py index e33bb2f..14abd1d 100644 --- a/aore/updater/xmlparser.py +++ b/aore/updater/xmlparser.py @@ -7,7 +7,8 @@ class XMLParser: def __init__(self, parse_function): self.parse_function = parse_function - def fast_iter(self, context, func, *args, **kwargs): + @staticmethod + def fast_iter(context, func, *args, **kwargs): for event, elem in context: # print event func(elem, *args, **kwargs) diff --git a/config.example.py b/config.example.py index 8c43376..cd24cc5 100644 --- a/config.example.py +++ b/config.example.py @@ -22,4 +22,5 @@ config.UnrarConfig.path = "C:\\Program Files\\WinRAR\\unrar.exe" config.Folders.temp = "E:\\!TEMP" config.BasicConfig.logging = True -config.BasicConfig.logfile = "pyphias.log" \ No newline at end of file +config.BasicConfig.debug_print = False +config.BasicConfig.logfile = "pyphias.log" diff --git a/requirements.txt b/requirements.txt index 669c6a1..d645905 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,9 @@ -lxml>=3.4.0 +lxml==3.6.0 bottle>=0.12.9 psycopg2>=2.6.1 PySimpleSOAP==1.16 python-Levenshtein==0.12.0 -enum34>=1.0.0 rarfile requests>=2.8.1 -soap2py==1.16 -sphinxapi +soap2py>=1.16 +sphinxapi>=2.2.9