Big commit for py3

This commit is contained in:
jar3b 2016-04-22 14:30:05 +03:00
parent 9ed372857e
commit c7f0de74a5
15 changed files with 103 additions and 63 deletions

View File

@ -1,7 +1,7 @@
# py-phias # py-phias
Python application that can operate with FIAS (Russian Address Object DB) 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 места на диске Для работы приложения необходимо достаточное кол-во RAM (1Gb+) и ~5.5Gb места на диске
@ -38,7 +38,7 @@ _Внимание_! Только Python 2.7+ (на 3+ не тестировал)
Предварительно обязательно установить и настроить: Предварительно обязательно установить и настроить:
1. Python 2.7.x, pip 1. Python 3, pip
Для Windows качаем - ставим, для Debian: Для Windows качаем - ставим, для Debian:
``` ```
sudo apt-get install python-setuptools sudo apt-get install python-setuptools
@ -83,12 +83,15 @@ _Внимание_! Только Python 2.7+ (на 3+ не тестировал)
4. Web-сервер с поддержкой WSGI, любой, по Вашему желанию. 4. Web-сервер с поддержкой WSGI, любой, по Вашему желанию.
### Windows ### 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 целиком). 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`, распакуйте его в удобное 4. Установить приложение, скачав релиз `https://github.com/jar3b/py-phias/archive/v0.0.2.zip`, распакуйте его в удобное
Вам место и запустите оттуда `python -m pip install -r requirements.txt` Вам место и запустите оттуда `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` для автостарта - если необходимо, добавьте `searchd --config /usr/local/sphinx/etc/sphinx.conf` в `/etc/rc.local` для автостарта
5. Настроим WSGI server, я использую nginx + passenger. Конфиг для passenger - [passenger_wsgi.py](passenger_wsgi.py), 5. Настроим WSGI server, я использую nginx + passenger. Конфиг для passenger - [passenger_wsgi.py](passenger_wsgi.py),
конфиг для nginx - [py-phias.conf](https://gist.github.com/jar3b/f8f5d351e0ea8ae2ed8e). Вы можете конфиг для nginx - [py-phias.conf](https://gist.github.com/jar3b/f8f5d351e0ea8ae2ed8e). Вы можете
использовать любое приемлемое сочетание. использовать любое приемлемое сочетание.
## Api
- `/normalize/<guid>` - актуализирует AOID или AOGUID, на выходе выдает
`{"aoid": "1d6185b5-25a6-4fe8-9208-e7ddd281926a"}`, где aoid - актуальный AOID.
- `/find/<text>` и `/find/<text>/strong`- полнотекстовый поиск по названию адресного объекта. `<text>` - строка поиска.
Если указан параметр `strong`, то поиск будет выдавать меньше результатов, но они будут точнее. Если же флаг не
указан, но будет выдано 10 наиболее релевантных результатов.
На выходе будет массив:
```
[
{
"cort": 0,
"text": "обл Псковская, р-н Порховский, д Гречушанка",
"ratio": 1537,
"aoid": "1d6185b5-25a6-4fe8-9208-e7ddd281926a"
},
... (up to 10)
]
```
,где cort - количество несовпавших слов, text - полное название адресного объекта, ratio - рейтинг, aoid -
актуальный AOID.

View File

@ -1,7 +1,5 @@
import os import os
import sys import sys
reload(sys)
cwd_dir = os.getcwd() cwd_dir = os.getcwd()
sys.path.append(cwd_dir) sys.path.append(cwd_dir)
sys.setdefaultencoding("utf-8")

View File

@ -3,6 +3,7 @@
class BasicConfig: class BasicConfig:
logging = False logging = False
debug_print = False
logfile = "" logfile = ""
def __init__(self): def __init__(self):

View File

@ -13,7 +13,7 @@ def violet_ratio(pattern, candidate):
for i in range(len(arr_pattern) - 1, -1, -1): for i in range(len(arr_pattern) - 1, -1, -1):
max_j = -1 max_j = -1
max_ratio = -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: for j in allowed_nums:
ratio = Levenshtein.ratio(arr_pattern[i], arr_candidate[j]) 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)) result.append(max_j*abs(max_ratio))
if max_j > -1: if max_j > -1:
allowed_nums.remove(max_j) del allowed_nums[max_j]
del arr_candidate[max_j] del arr_candidate[max_j]
return sum(result) - len(arr_candidate) return sum(result) - len(arr_candidate)

View File

@ -22,11 +22,11 @@ class SphinxHelper:
os.makedirs(Folders.temp) os.makedirs(Folders.temp)
# оздаем 3 папки для Сфинкса # оздаем 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') 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') 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') os.makedirs(SphinxConfig.var_dir + '/data')
def configure_indexer(self, indexer_binary, config_filename): def configure_indexer(self, indexer_binary, config_filename):
@ -55,7 +55,7 @@ class SphinxHelper:
logging.info("All indexes were created.") logging.info("All indexes were created.")
# remove temp files # remove temp files
for fname, fpath in self.files.iteritems(): for fname, fpath in self.files.items():
try: try:
os.remove(fpath) os.remove(fpath)
except: except:
@ -84,10 +84,10 @@ class SphinxHelper:
def __dbexport_sugg_dict(self): def __dbexport_sugg_dict(self):
logging.info("Place suggestion dict to DB %s...", self.files['dict.txt']) 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 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 line = None
while line != '': while line != '':
nodes = [] nodes = []
@ -111,9 +111,11 @@ class SphinxHelper:
except: except:
pass 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.") logging.info("Done.")
return fname
def __create_ao_index_config(self): def __create_ao_index_config(self):
fname = os.path.abspath(Folders.temp + "/addrobj.conf") fname = os.path.abspath(Folders.temp + "/addrobj.conf")
logging.info("Creating config %s", fname) logging.info("Creating config %s", fname)
@ -156,7 +158,7 @@ class SphinxHelper:
sphinx_var_path=SphinxConfig.var_dir) sphinx_var_path=SphinxConfig.var_dir)
f = open(out_filename, "w") f = open(out_filename, "w")
for fname, fpath in self.files.iteritems(): for fname, fpath in self.files.items():
if ".conf" in fname: if ".conf" in fname:
with open(fpath, "r") as conff: with open(fpath, "r") as conff:
for line in conff: for line in conff:

View File

@ -5,7 +5,7 @@ import logging
from bottle import response from bottle import response
from aore.search.fiasfactory import FiasFactory from aore.search.fiasfactory import FiasFactory
from miscutils.bottlecl import BottleCL from .miscutils.bottlecl import BottleCL
class App(BottleCL): class App(BottleCL):

View File

@ -1,16 +1,16 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging import logging
import re import re
import urllib import traceback
import urllib.parse
from uuid import UUID from uuid import UUID
import psycopg2 import psycopg2
import traceback
from bottle import template from bottle import template
from aore.config import DatabaseConfig, BasicConfig from aore.config import DatabaseConfig, BasicConfig
from aore.dbutils.dbimpl import DBImpl from aore.dbutils.dbimpl import DBImpl
from search import SphinxSearch from .search import SphinxSearch
class FiasFactory: class FiasFactory:
@ -38,10 +38,10 @@ class FiasFactory:
if rule == "boolean": if rule == "boolean":
assert isinstance(param, bool), "Invalid parameter type" assert isinstance(param, bool), "Invalid parameter type"
if rule == "uuid": 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" param), "Invalid parameter value"
if rule == "text": 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" assert len(param) > 3, "Text too short"
pattern = re.compile(r"[A-za-zА-Яа-я \-,.#№]+") pattern = re.compile(r"[A-za-zА-Яа-я \-,.#№]+")
assert pattern.match(param), "Invalid parameter value" assert pattern.match(param), "Invalid parameter value"
@ -52,15 +52,17 @@ class FiasFactory:
def find(self, text, strong=False): def find(self, text, strong=False):
try: try:
text = urllib.unquote(text).decode('utf8') text = urllib.parse.unquote(str(text))
self.__check_param(text, "text") self.__check_param(text, "text")
self.__check_param(strong, "boolean") self.__check_param(strong, "boolean")
results = self.searcher.find(text, strong) results = self.searcher.find(text, strong)
except Exception, err: except Exception as err:
if BasicConfig.logging: if BasicConfig.logging:
logging.error(traceback.format_exc(err)) logging.error(traceback.format_exc())
return dict(error=err.args[0]) if BasicConfig.debug_print:
traceback.print_exc()
return dict(error=str(err))
return results return results
@ -73,10 +75,12 @@ class FiasFactory:
rows = self.db.get_rows(sql_query, True) rows = self.db.get_rows(sql_query, True)
assert len(rows), "Record with this AOID not found in DB" assert len(rows), "Record with this AOID not found in DB"
except Exception, err: except Exception as err:
if BasicConfig.logging: if BasicConfig.logging:
logging.error(traceback.format_exc(err)) logging.error(traceback.format_exc())
return dict(error=err.args[0]) if BasicConfig.debug_print:
traceback.print_exc()
return dict(error=str(err))
if len(rows) == 0: if len(rows) == 0:
return [] return []
@ -94,10 +98,12 @@ class FiasFactory:
sql_query = self.expand_templ.replace("//aoid", normalized_id) sql_query = self.expand_templ.replace("//aoid", normalized_id)
rows = self.db.get_rows(sql_query, True) rows = self.db.get_rows(sql_query, True)
except Exception, err: except Exception as err:
if BasicConfig.logging: if BasicConfig.logging:
logging.error(traceback.format_exc(err)) logging.error(traceback.format_exc())
return dict(error=err.args[0]) if BasicConfig.debug_print:
traceback.print_exc()
return dict(error=str(err))
return rows return rows
@ -111,9 +117,11 @@ class FiasFactory:
rows = self.db.get_rows(sql_query, True) rows = self.db.get_rows(sql_query, True)
assert len(rows), "Record with this AOID not found in DB" assert len(rows), "Record with this AOID not found in DB"
except Exception, err: except Exception as err:
if BasicConfig.logging: if BasicConfig.logging:
logging.error(traceback.format_exc(err)) logging.error(traceback.format_exc())
return dict(error=err.args[0]) if BasicConfig.debug_print:
traceback.print_exc()
return dict(error=str(err))
return rows return rows

View File

@ -11,8 +11,8 @@ from aore.config import SphinxConfig
from aore.miscutils.exceptions import FiasException from aore.miscutils.exceptions import FiasException
from aore.miscutils.fysearch import violet_ratio from aore.miscutils.fysearch import violet_ratio
from aore.miscutils.trigram import trigram from aore.miscutils.trigram import trigram
from wordentry import WordEntry from .wordentry import WordEntry
from wordvariation import VariationType from .wordvariation import VariationType
class SphinxSearch: class SphinxSearch:
@ -62,7 +62,7 @@ class SphinxSearch:
self.client_show.SetSortMode(sphinxapi.SPH_SORT_EXTENDED, "krank DESC") self.client_show.SetSortMode(sphinxapi.SPH_SORT_EXTENDED, "krank DESC")
def __get_suggest(self, word, rating_limit, count): def __get_suggest(self, word, rating_limit, count):
word_len = str(len(word) / 2) word_len = len(word)
trigrammed_word = '"{}"/1'.format(trigram(word)) trigrammed_word = '"{}"/1'.format(trigram(word))
self.__configure(SphinxConfig.index_sugg, word_len) self.__configure(SphinxConfig.index_sugg, word_len)
@ -95,7 +95,7 @@ class SphinxSearch:
return outlist return outlist
# Получает список объектов (слово) # Получает список объектов (слово)
def __get_word_entries(self, words, strong): def __get_word_entries(self, words):
we_list = [] we_list = []
for word in words: for word in words:
if word != '': if word != '':
@ -111,14 +111,14 @@ class SphinxSearch:
def find(self, text, strong): def find(self, text, strong):
def split_phrase(phrase): def split_phrase(phrase):
phrase = unicode(phrase).lower() phrase = phrase.lower()
return re.split(r"[ ,:.#$]+", phrase) return re.split(r"[ ,:.#$]+", phrase)
# сплитим текст на слова # сплитим текст на слова
words = split_phrase(text) words = split_phrase(text)
# получаем список объектов (слов) # получаем список объектов (слов)
word_entries = self.__get_word_entries(words, strong) word_entries = self.__get_word_entries(words)
word_count = len(word_entries) word_count = len(word_entries)
# проверяем, есть ли вообще что-либо в списке объектов слов (или же все убрали как частое) # проверяем, есть ли вообще что-либо в списке объектов слов (или же все убрали как частое)
@ -169,7 +169,7 @@ class SphinxSearch:
parsed_ids.append(match['attrs']['aoid']) parsed_ids.append(match['attrs']['aoid'])
results.append( results.append(
dict(aoid=match['attrs']['aoid'], dict(aoid=match['attrs']['aoid'],
text=unicode(match['attrs']['fullname']), text=str(match['attrs']['fullname']),
ratio=match['attrs']['krank'], ratio=match['attrs']['krank'],
cort=i)) cort=i))

View File

@ -5,6 +5,10 @@ from aore.config import SphinxConfig
from aore.search.wordvariation import WordVariation, VariationType from aore.search.wordvariation import WordVariation, VariationType
def cleanup_string(word):
return word.replace('-', '').replace('@', '').replace('#', '')
class WordEntry: class WordEntry:
# Варианты распеределния для слов с первыми двумя символами, где: # Варианты распеределния для слов с первыми двумя символами, где:
# 0 - не найдено, 1 - найдено одно, x - найдено много (>1) # 0 - не найдено, 1 - найдено одно, x - найдено много (>1)
@ -51,14 +55,14 @@ class WordEntry:
def __init__(self, db, word): def __init__(self, db, word):
self.db = db self.db = db
self.bare_word = str(word) self.bare_word = word
self.word = self.__cleanify(self.bare_word) self.word = cleanup_string(self.bare_word)
self.word_len = len(unicode(self.word)) self.word_len = len(self.word)
self.parameters = dict(IS_FREQ=False, SOCR_WORD=None) self.parameters = dict(IS_FREQ=False, SOCR_WORD=None)
self.ranks = self.__init_ranks() 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 self.__dict__[mt_name] = False
for mt_value in mt_values: for mt_value in mt_values:
self.__dict__[mt_name] = self.__dict__[mt_name] or re.search(mt_value, self.ranks) 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_LAST_STAR = False
self.MT_AS_IS = True self.MT_AS_IS = True
def __cleanify(self, word):
return word.replace('-', '').replace('@', '')
def variations_generator(self, strong, suggestion_func): def variations_generator(self, strong, suggestion_func):
default_var_type = VariationType.normal default_var_type = VariationType.normal
# Если слово встречается часто, ставим у всех вариантов тип VariationType.freq # Если слово встречается часто, ставим у всех вариантов тип VariationType.freq
@ -115,8 +116,9 @@ class WordEntry:
"UNION ALL SELECT COUNT(*), NULL FROM \"AOTRIG\" WHERE word='{}' " \ "UNION ALL SELECT COUNT(*), NULL FROM \"AOTRIG\" WHERE word='{}' " \
"UNION ALL SELECT COUNT(*), MAX(scname) FROM \"SOCRBASE\" WHERE socrname ILIKE '{}'" \ "UNION ALL SELECT COUNT(*), MAX(scname) FROM \"SOCRBASE\" WHERE socrname ILIKE '{}'" \
"UNION ALL SELECT COUNT(*), NULL FROM \"SOCRBASE\" WHERE scname ILIKE '{}'" \ "UNION ALL SELECT COUNT(*), NULL FROM \"SOCRBASE\" WHERE scname ILIKE '{}'" \
"UNION ALL SELECT frequency, NULL FROM \"AOTRIG\" WHERE word='{}';".format( "UNION ALL SELECT frequency, NULL FROM \"AOTRIG\" WHERE word='{}';".format(self.word, self.word_len,
self.word, self.word_len, self.word, self.bare_word, self.bare_word, self.word) self.word, self.bare_word,
self.bare_word, self.word)
result = self.db.get_rows(sql_qry) result = self.db.get_rows(sql_qry)

View File

@ -9,7 +9,7 @@ import requests
from aore.config import Folders, UnrarConfig from aore.config import Folders, UnrarConfig
from aore.miscutils.exceptions import FiasException from aore.miscutils.exceptions import FiasException
from aoxmltableentry import AoXmlTableEntry from .aoxmltableentry import AoXmlTableEntry
class AoRar: class AoRar:

View File

@ -2,6 +2,7 @@
from pysimplesoap.client import SoapClient from pysimplesoap.client import SoapClient
class SoapReceiver: class SoapReceiver:
def __init__(self): def __init__(self):
self.client = SoapClient( self.client = SoapClient(

View File

@ -62,9 +62,9 @@ class Updater:
mode = None mode = None
while not mode: while not mode:
try: try:
mode = int(raw_input('Enter FIAS update version (3 digit):')) mode = int(input('Enter FIAS update version (3 digit):'))
except ValueError: except ValueError:
print "Not a valid fias version, try again." print("Not a valid fias version, try again.")
return mode return mode

View File

@ -7,7 +7,8 @@ class XMLParser:
def __init__(self, parse_function): def __init__(self, parse_function):
self.parse_function = 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: for event, elem in context:
# print event # print event
func(elem, *args, **kwargs) func(elem, *args, **kwargs)

View File

@ -22,4 +22,5 @@ config.UnrarConfig.path = "C:\\Program Files\\WinRAR\\unrar.exe"
config.Folders.temp = "E:\\!TEMP" config.Folders.temp = "E:\\!TEMP"
config.BasicConfig.logging = True config.BasicConfig.logging = True
config.BasicConfig.logfile = "pyphias.log" config.BasicConfig.debug_print = False
config.BasicConfig.logfile = "pyphias.log"

View File

@ -1,10 +1,9 @@
lxml>=3.4.0 lxml==3.6.0
bottle>=0.12.9 bottle>=0.12.9
psycopg2>=2.6.1 psycopg2>=2.6.1
PySimpleSOAP==1.16 PySimpleSOAP==1.16
python-Levenshtein==0.12.0 python-Levenshtein==0.12.0
enum34>=1.0.0
rarfile rarfile
requests>=2.8.1 requests>=2.8.1
soap2py==1.16 soap2py>=1.16
sphinxapi sphinxapi>=2.2.9