Страницы

Поиск по вопросам

среда, 3 октября 2018 г.

Почему буквы алфавита с “р” по “ю” не входят в диапазон “а-я”?

Поясню сразу: я знаю, что python 2 требует явного объявления строк как юникодовых. Понимаю, что это и не должно работать корректно. Мне интересна "анатомия" поломки. Что именно внутри re.compile() и regex.search() производит такой результат?

Судя по нижеприведённому коду, диапазон 'а-яё' не включает в себя диапазон 'р-ю', зато диапазон 'р-ю' включает 'ё'.
mcve.py:
# coding=utf-8
import re
# Это панграмма, она содержит все буквы алфавита test = 'широкая электрификация южных губерний даст мощный толчок подъёму сельского хозяйства'
regex1 = re.compile('[а-яА-ЯёЁ\s]+') regex2 = re.compile('[а-яА-ЯёЁшьрэтфцюыхущчъ\s]+') regex3 = re.compile('[а-яёшьрэтфцюыхущчъс\s]+') regex4 = re.compile('[а-яр-ю\s]+')
print regex1.search(test).group() print regex2.search(test).group() print regex3.search(test).group() print regex4.search(test).group()
Результат:
� широкая электрификация южных губерний даст мощный толчок подъёму сельского хозяйства широкая электрификация южных губерний даст мощный толчок подъёму сельского хозяйства широкая электрификация южных губерний даст мощный толчок подъёму сельского хозяйства
Я убедился, что все буквы алфавита от "А" до "Я" и от "а" до "я" идут в Юникоде подряд, кроме "Ёё", которые явным образом добавлены в регулярное выражение.
Постепенно добавляя буквы, на которых прерывается поиск по первому выражению, я пришёл к диапазону а-яА-ЯёЁшьрэтфцюыхущчъ. Если отсортировать добавленные буквы, получается почти сплошной интервал: "ртуфхцчшщъыьэю"
Если убрать заглавные буквы, т.е. "[А-ЯЁ]", то неожиданным образом поиск прерывается на "с". Интервал становится сплошным: с "р" и до "ю". Это regex3
И наконец оказывается, что теперь интервал можно свернуть и даже убрать "ё" (regex4).
Что вообще происходит?
python --version Python 2.7.6

Если явным образом сделать юникодовую строку и регулярное выражение, то всё работает как должно. Но ведь как-то работает и без этого. Объясните, как?
test2 = u'широкая электрификация южных губерний даст мощный толчок подъёму сельского хозяйства' regex5 = re.compile(u'[а-яА-ЯёЁ\s]+')


Ответ

Самый простой способ посмотреть, что же не так с регуляркой - поставить debug флаг. Причем в таком случае абсолютно нет нужды лезть в потроха - они покажут то же самое. В этом можно убедиться, если залезть руками в %python_folder%/Lib/sre_parse.py и добавить там пару print-ов на строку 438 - выглядит она как-то тактак:
elif this == "[": # character set set = [] setappend = set.append ## if sourcematch(":"): ## pass # handle character classes if sourcematch("^"): setappend((NEGATE, None)) # check remaining characters start = set[:] while 1: this = sourceget() if len(this) == 1: # Вот ровно в этом месте парсер перебирает содержимое [] print(source.tell(), "ORD: ", ord(this)) if this == "]" and set != start: break
Так вот этот print покажет все абсолютно то же самое, что и debug - то что буковки на самом деле не буковки.
regex1 = re.compile('[а-яА-ЯёЁ\s]+', re.DEBUG)
max_repeat 1 2147483647 in literal 208 range (176, 209) literal 143 literal 208 range (144, 208) literal 175 literal 209 literal 145 literal 208 literal 129 category category_space
Сразу видно, что что-то совсем нечисто, потому что первым должен быть range(ord('a')-ord('я'), а вместо него чепуха какая-то. А все от того, что строки в кодировке UTF8 (указана явно в файле), а тип у них - байты. Они могут отображаться нормально в терминале, если кодировки совпадают. Я пользуюсь Pycharm и его консоль в UTF8. Но если бы я запустил то же самое на стандартном терминале винды, то отобразилась бы натурально каша (типа такой - ╨Я╤А╨╕╨▓╨╡╤В), ибо кодировка CP866.
Например,
print("Привет") # НО! for char in "Привет": print(ord(char), repr(char))
В debug выводе число 208 - это первая половинка UTF8 символа 'а' - 176 - вторая половинка, затем следует дефис и снова первая половинка символа 'я' - в этом можно убедиться, открыв исходник в любом HEX редакторе. В итоге неверный интервал. Соответственно, когда парсер перебирает содержимое квадратных скобок он натыкается не на буквы, а на байты, а точнее на половинки букв в кодировке UTF8. Можно симулировать поведение регулярки таким кодом:
reg_min = 144 reg_max = 209
result = [] for char in test: ordedr = ord(char) if ordedr >= reg_min and ordedr <= reg_max: result.append(char) else: break print(ord(regex1.search(test).group())) print(list(map(ord, result)))
>>> 209 >>> [209]

Комментариев нет:

Отправить комментарий