Skip to content

Commit 9be1fff

Browse files
committed
Full code update
1 parent c9d7cfa commit 9be1fff

File tree

93 files changed

+15337
-5
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+15337
-5
lines changed

README.md

+125-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,132 @@
1-
plainrussian
1+
Plain Russian Language / Понятный (простой) русский язык.
22
============
33

4-
Plain Russian Language / Понятный (простой) русский язык.
4+
# Зачем всё это нужно
5+
Оценка читаемости текстов необходима для автоматического определения сложности текстов на русском языке.
6+
7+
# Что было сделано
8+
Есть 5 американских алгоритмов оценки читаемости текстов, это:
9+
* Flesch-Kinkaid - http://en.wikipedia.org/wiki/Flesch%E2%80%93Kincaid_readability_tests
10+
* Dale-Chale readability formula - http://en.wikipedia.org/wiki/Dale%E2%80%93Chall_readability_formula
11+
* Coleman-Liau index - http://en.wikipedia.org/wiki/Coleman%E2%80%93Liau_index
12+
* SMOG - http://en.wikipedia.org/wiki/SMOG
13+
* Automated Readability Index - http://en.wikipedia.org/wiki/Automated_Readability_Index
14+
15+
Были накоплены тексты на русском языке с разметками по уровню чтения, это:
16+
* тексты для внеклассного чтения;
17+
* экспертно размеченные взрослые тексты;
18+
* особо сложные тексты законов;
19+
* и так далее.
20+
21+
Все алгоритмы были обучены под русский язык - специальным образом каждая формула была подобрана на основе обучающей выборки.
22+
Для всех формул были применены коэффициенты позволяющие применять их к русским текстам.
23+
24+
На базе этих формул был сделан специальный веб-сервис который позволяет передавать ему текст или ссылку и оценивать его на сложность.
25+
26+
# Как работает API
27+
28+
API доступно по ссылке и http://api.plainrussian.ru/api/1.0/ru/measure/
29+
и для его работы ему необходимо передать параметр url (для ссылки) или text (как текст).
30+
31+
Параметр url передается при обращении через GET запрос, пример такого обращения выглядит вот так:
32+
- http://api.plainrussian.ru/api/1.0/ru/measure/?url=http://minsvyaz.ru/ru/news/index.php?id_4=44264
33+
34+
вот с примером простого текста:
35+
- http://api.plainrussian.ru/api/1.0/ru/measure/?url=http://www.anekdot.ru/id/674877/
36+
37+
или вот:
38+
- http://api.plainrussian.ru/api/1.0/ru/measure/?url=http://www.gosuslugi.ru/pgu/cms/content/isr/view/00000000000/290/309&debug=1
39+
40+
Результат выглядит вот так:
41+
42+
` `
43+
`{`
44+
`metrics: `
45+
`{`
46+
`wsyllabes: `
47+
`{`
48+
`1: 94,`
49+
`2: 116,`
50+
`3: 140,`
51+
`4: 87,`
52+
`5: 139,`
53+
`6: 45,`
54+
`7: 18,`
55+
`8: 4,`
56+
`15: 1`
57+
`},`
58+
`c_share: 32.142857142857146,`
59+
`chars: 6000,`
60+
`avg_slen: 46,`
61+
`spaces: 510,`
62+
`n_syllabes: 2232,`
63+
`n_words: 644,`
64+
`letters: 5170,`
65+
`n_sentences: 14,`
66+
`n_complex_words: 207,`
67+
`n_simple_words: 437,`
68+
`avg_syl: 3.4658385093167703`
69+
`},`
70+
`status: 0,`
71+
`indexes: `
72+
`{`
73+
`grade_SMOG: "Аспирантура, второе высшее образование, phD",`
74+
`grade_ari: "Аспирантура, второе высшее образование, phD",`
75+
`index_fk: 33.342906832298134,`
76+
`grade_cl: "Аспирантура, второе высшее образование, phD",`
77+
`grade_fk: "Аспирантура, второе высшее образование, phD",`
78+
`index_cl: 23.062857142857148,`
79+
`grade_dc: "Аспирантура, второе высшее образование, phD",`
80+
`index_dc: 30.300857142857147,`
81+
`index_ari: 32.11796894409938,`
82+
`index_SMOG: 34.046178356649776`
83+
`}`
84+
`} `
85+
86+
Кроме того, вместо параметра url можно использовать text, чтобы при запросе передавался текст, а не гиперссылка на текст. Вместо GET-запроса имеет смысл использовать POST, чтобы обойти ограничение на размер URI.
87+
Пример того, как это выглядит в Python с использованием библиотеки requests:
88+
89+
import requests
90+
text = "Здесь может быть Ваш текст"
91+
response = requests.post("http://api.plainrussian.ru/api/1.0/ru/measure/", data={"text":text})
92+
response.json()
93+
94+
Параметры означают:
95+
## indexes - набор индикаторов читаемости текста:
96+
* grade_SMOG - уровень образования необходимый для понимания текста по формуле SMOG, человеческим языком
97+
* grade_ari - уровень образования необходимый для понимания текста по формуле Automated Readability Index, человеческим языком
98+
* grade_cl - уровень образования необходимый для понимания текста по формуле Coleman-Liau, человеческим языком
99+
* grade_fk - уровень образования необходимый для понимания текста по формуле Flesch-Kinkaid, человеческим языком
100+
* grade_dc - уровень образования необходимый для понимания текста по формуле Dale-Chale, человеческим языком
101+
* index_SMOG - уровень образования необходимый для понимания текста по формуле SMOG, в годах обучения от 1 до бесконечности
102+
* index_ari - уровень образования необходимый для понимания текста по формуле Automated Readability Index, в годах обучения от 1 до бесконечности
103+
* index_cl - уровень образования необходимый для понимания текста по формуле Coleman-Liau, в годах обучения от 1 до бесконечности
104+
* index_fk - уровень образования необходимый для понимания текста по формуле Flesch-Kinkaid, в годах обучения от 1 до бесконечности
105+
* index_dc - уровень образования необходимый для понимания текста по формуле Dale-Chale, в годах обучения от 1 до бесконечности
106+
107+
## metrics - набор расчетных показателей из текста
108+
* chars - сколько всего знаков тексте
109+
* spaces - сколько пробелов в тексте
110+
* letters - сколько букв в тексте
111+
* n_words - число слов
112+
* n_sentences - число предложений
113+
* n_complex_words - число слов с более чем 4-мя слогами
114+
* n_simple_words - число слов до 4-х слогов включительно
115+
* avg_slen - среднее число слов на предложение
116+
* avg_syl - среднее число слогов на предложение
117+
* c_share - процент сложных слов от общего числа
118+
* w_syllabes - словарь из значений: число слогов и число слов с таким числом слогов в этом тексте
119+
120+
Если передать параметр debug=1, то также вернется значение текста которое было передано.
121+
122+
Вот несколько примеров текстов на которых шло обучение.
123+
- Бианки "Лесной дом", 1-й класс - http://api.plainrussian.ru/api/1.0/ru/measure/?url=http://plainrussian.ru/textsbygrade/1/bianki_lesdom.txt
124+
- Астафьев "Солдат", 9-й класс - http://api.plainrussian.ru/api/1.0/ru/measure/?url=http://plainrussian.ru/textsbygrade/9/astafiev_soldier.txt
125+
и так много документов.
126+
5127

6-
Данный проект создан для того чтобы собрать в одном месте разрозненный код, тексты и материалы по понятности / простоте / ясности русского языка.
7128

8-
* readability.io - доступное API сервиса readability.io - https://github.com/ivbeg/readability.io/wiki/API
9-
* textmetric - примеры текстов и метрика расчета читаемости текстов.
129+
* textmetric - библиотека кода для измерения простоты русского языка
10130

11131

12132
Текстовые файлы в textmetric - это специально подобранные тексты с предварительными возрастными пометками. Это позволяет разрабатывать собственные алгоритмы анализа читабельности, простоты, понятности текстов на базе этих метрик.

api/readabilityio.py

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import tornado.httpserver
2+
import tornado.ioloop
3+
import tornado.web
4+
#import memcache
5+
import chardet
6+
import urllib
7+
import html2text
8+
import requests
9+
import json
10+
from django.utils.feedgenerator import Rss201rev2Feed, Atom1Feed
11+
from pymongo import Connection
12+
from readability.readability import Document
13+
from textmetric.metric import calc_readability_metrics
14+
import time
15+
16+
READ_DB = 'readability'
17+
LOG_COLL = 'log'
18+
19+
ERROR_NONE = 0
20+
ERROR_INVALID_DATA = 101
21+
22+
class RusMeasureHandler(tornado.web.RequestHandler):
23+
def initialize(self):
24+
self.conn = Connection()
25+
self.db = self.conn[READ_DB]
26+
self.log = self.db[LOG_COLL]
27+
self.log.ensure_index("reqtime", 1)
28+
29+
def __log(self, logrec):
30+
self.conn = Connection()
31+
self.db = self.conn[READ_DB]
32+
self.log = self.db[LOG_COLL]
33+
self.log.save(logrec)
34+
35+
36+
def get(self):
37+
rtime = time.time()
38+
url = self.get_argument('url')
39+
lang = self.get_argument('lang', 'ru')
40+
debug = self.get_argument('debug', "0")
41+
debug = int(debug) if debug.isdigit() else 0
42+
r = requests.get(url)
43+
ctype = r.headers['content-type'].lower() if 'content-type' in r.headers.keys() else 'text/html'
44+
print ctype
45+
ctype = ctype.split(';', 1)[0]
46+
if ctype == 'text/html':
47+
ht = html2text.HTML2Text()
48+
ht.ignore_links = True
49+
ht.ignore_images = True
50+
ht.ignore_emphasis = True
51+
text = ht.handle(Document(r.text).summary())
52+
status = ERROR_NONE
53+
elif ctype == 'text/plain':
54+
55+
print type(r.content)
56+
text = r.content.decode('utf8', 'ignore')
57+
# text = r.text.decode('utf8', 'ignore')
58+
status = ERROR_NONE
59+
else:
60+
text = None
61+
status = ERROR_INVALID_DATA
62+
# text = text.decode('utf8')
63+
if status == ERROR_NONE:
64+
results = calc_readability_metrics(text)
65+
else:
66+
results = {'lang' : lang, 'debug' : debug}
67+
if debug:
68+
results['debug'] = {'text' : text}
69+
results['status'] = status
70+
self.set_header("Content-Type", "application/json")
71+
self.write(json.dumps(results, indent=4))
72+
etime = time.time() - rtime
73+
logreq = results.copy()
74+
logreq['text'] = text
75+
logreq['reqtime'] = rtime
76+
logreq['time'] = etime
77+
self.__log(logreq)
78+
79+
def post(self):
80+
rtime = time.time()
81+
text = self.get_argument('text')
82+
lang = self.get_argument('lang', 'ru')
83+
debug = self.get_argument('debug', "0")
84+
results = calc_readability_metrics(text)
85+
results['status'] = ERROR_NONE
86+
results['debug'] = debug
87+
results['lang'] = lang
88+
self.set_header("Content-Type", "application/json")
89+
self.write(json.dumps(results, indent=4))
90+
etime = time.time() - rtime
91+
logreq = results.copy()
92+
logreq['text'] = text
93+
logreq['reqtime'] = rtime
94+
logreq['time'] = etime
95+
self.__log(logreq)
96+
97+
98+
99+
100+
application = tornado.web.Application([
101+
(r"/api/1.0/ru/measure/", RusMeasureHandler),
102+
])
103+
104+
if __name__ == "__main__":
105+
http_server = tornado.httpserver.HTTPServer(application)
106+
http_server.listen(9888)
107+
tornado.ioloop.IOLoop.instance().start()

api/textmetric/LR.py

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
from sklearn.linear_model import LinearRegression
2+
import csv
3+
from math import sqrt
4+
debugMode = True
5+
6+
###########################################################################
7+
# PART 1: PREPARATIONS #
8+
###########################################################################
9+
10+
def getMetricsFromCSV():
11+
"""
12+
Reads 'metrics.csv'.
13+
Returns a list of dictionaries (one for each row).
14+
"""
15+
with open('metrics.csv') as csvfile:
16+
reader = csv.DictReader(csvfile)
17+
listOfDicts = [row for row in reader]
18+
19+
for d in listOfDicts:
20+
for key in d:
21+
if key in ['filename', 'name']: pass # str
22+
elif key == 'grade': d[key] = int(d[key]) # int
23+
elif key == 'wsyllabes': d[key] = eval(d[key]) # dict
24+
else: d[key] = float(d[key]) # float
25+
26+
return listOfDicts
27+
28+
###########################################################################
29+
30+
def calculateCoefficients(xs, ys):
31+
"""
32+
Takes a list of xs and a list of ys, for example:
33+
* xs: [ (1.2, 3.4), (1.5, 3.3), ... ]
34+
* ys: [ 3, 5, ... ]
35+
36+
Returns a list of coefficients and an intercept,
37+
such that (hopefully):
38+
y = x[0] * coef_[0] + x[1] * coef_[1] + intercept
39+
"""
40+
model = LinearRegression()
41+
model.fit(xs, ys)
42+
return (model.coef_, model.intercept_)
43+
44+
###########################################################################
45+
# PART 2: EVALUATION #
46+
###########################################################################
47+
48+
def checkPredictions(grades, IB_predictions, KD_predictions):
49+
"""
50+
Takes a list of real grades and two lists of predictions.
51+
Compares the accuracy of predictions.
52+
"""
53+
length = len(grades)
54+
assert len(IB_predictions) == len(KD_predictions) == length
55+
56+
IB_errors = [ pair[0]-pair[1] for pair in zip(IB_predictions, grades)]
57+
KD_errors = [ pair[0]-pair[1] for pair in zip(KD_predictions, grades)]
58+
59+
IB_sum_of_errors = sum( abs(e) for e in IB_errors )
60+
KD_sum_of_errors = sum( abs(e) for e in KD_errors )
61+
print("Сумма отклонений (ИБ): ", "%.2f" % IB_sum_of_errors)
62+
print("Сумма отклонений (КД): ", "%.2f" % KD_sum_of_errors)
63+
64+
#https://ru.wikipedia.org/wiki/Абсолютное_отклонение
65+
IB_mean_abs_error = IB_sum_of_errors / length
66+
KD_mean_abs_error = KD_sum_of_errors / length
67+
print("Среднее абс. отклонение (ИБ): ", "%.2f" % IB_mean_abs_error)
68+
print("Среднее абс. отклонение (КД): ", "%.2f" % KD_mean_abs_error)
69+
70+
#https://ru.wikipedia.org/wiki/Среднеквадратическое_отклонение
71+
IB_sum_of_squares = sum( e**2 for e in IB_errors )
72+
KD_sum_of_squares = sum( e**2 for e in KD_errors )
73+
IB_mean_sq_error = sqrt( IB_sum_of_squares / length )
74+
KD_mean_sq_error = sqrt( KD_sum_of_squares / length )
75+
print("Среднее кв. отклонение (ИБ): ", "%.2f" % IB_mean_sq_error)
76+
print("Среднее кв. отклонение (КД): ", "%.2f" % KD_mean_sq_error)
77+
78+
###########################################################################
79+
# PART 3: SPECIFIC METRICS #
80+
###########################################################################
81+
82+
def fit_Flesch_Kincaid_grade(listOfDicts):
83+
"""
84+
Takes data as a list of dictionaries created by 'getMetricsFromCSV()'.
85+
Returns parameters for F-K formula that best fit the data.
86+
87+
If debugMode is on, checks the accuracy of predictions.
88+
"""
89+
90+
xs = [ (d['avg_slen'], d['avg_syl']) for d in listOfDicts ]
91+
ys = [ d['grade'] for d in listOfDicts ]
92+
93+
coeffs, intercept = calculateCoefficients(xs, ys)
94+
95+
if debugMode:
96+
print("FLESCH-KINCAID GRADE (KD):")
97+
print("GRADE = {:.2f} * {} + {:.2f} * {} + {:.2f}".format(
98+
coeffs[0], 'avg_syl', coeffs[1], 'avg_slen', intercept))
99+
100+
IB_predictions = [ d['index_fk_rus']
101+
for d in listOfDicts ]
102+
KD_predictions = [ x[0]*coeffs[0] + x[1]*coeffs[1] + intercept
103+
for x in xs ]
104+
checkPredictions(ys, IB_predictions, KD_predictions)
105+
106+
return coeffs, intercept
107+
108+
###########################################################################
109+
110+
if __name__ == "__main__":
111+
112+
listOfDicts = getMetricsFromCSV()
113+
fit_Flesch_Kincaid_grade(listOfDicts)
114+
115+

api/textmetric/__init__.py

Whitespace-only changes.

api/textmetric/__init__.pyc

149 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)