skip to navigation
skip to content

feed icon RSS хранилка

Решения на трета задача

Публикувано на 28.04.2008 18:33 от Стефан
Последна промяна на 28.04.2008 18:36
Тази публикация е от предишно издание на курса, моля не разчитайте на актуалността на информацията.

Признаваме си: трета задача не беше от най-лесните. Въпреки това, беше съвсем близка до реален проблем. И имахте голяма свобода при решаването й. Убедени сме, че за част от вас е била приятно занимане, а друга част са научили много.

Една от кукичките беше елегантните решения. Предложихме ви бонус точки ако се постараете да напишете по-красив код и ни уведомите за това. Част от вас го направиха. Тук ви предлагаме няколко от тези решения, заедно с наши коментари към тях.

Като цяло

Задачата може да се реши с много простичка схема, ползваща само регулярни изрази. Ето как изглежда:

  1. Ограждате текста с <p>...</p>
  2. Прилагате заместване за получер, курсив и адреси (именовани, ненаименовани и пощенски).
  3. Замествате всяко заглавие с </p><hX>...</hX><p>
  4. Замествате всяка последователност от повече от два нови реда с </p><p>
  5. Замествате единичните нови редове с <br />
  6. Чистите празните параграфи и параграфите, които съдържат само нови редове.
  7. Чистите новия ред в края и началото на параграф

Това може да стане само с регулярни изрази и е сравнително добро упражнение. Ще ви покажем няколко решения, които ползват вариация на горния алгоритъм.

Що годе…

def wiki(text):
	result = re.sub('\\r\\n|\\r', '\\n', text) # Normalizes new lines

	headingMatcher = re.compile('^[ \\t]*(?P<level>={1,6})[ \\t]*(?P<content>[^\\\\s].*?)[ \\t]*(?P=level)[ \\t]*$', re.M)
	newParagraphMatcher = re.compile('\\n{2,}')
	lineBreakMatcher = re.compile('\\n')
	emphasisMatcher = re.compile(r'_(?P<word>\\w+)_', re.U)
	strongEmphasisMatcher = re.compile(r'[*](?P<word>\\w+)[*]', re.U) 1
	namedURLMatcher = re.compile(r'\\[(?P<url>http://.*?)[ ]+(?P<name>[^ ].*?)\\]') 2
	urlMatcher = re.compile(r'(?<=[\\s>])(?P<url>http://.*?)(?=[\\s<])')
	emailMatcher = re.compile(r'(?<=[\\s>])(?P<email>[\\w_+.]+?@[\\w_+.]+[.][\\w_+]+?)(?=[^\\w_+.])', re.U)

	def createHeading(match):
		return "</p><h%d>%s</h%d><p>" % (len(match.group("level")), match.group("content"), len(match.group("level"))) 3

	result = headingMatcher.sub(createHeading, result)
	result = '<p>' + result.strip() + '</p>'
	result = newParagraphMatcher.sub('</p><p>', result)
	result = lineBreakMatcher.sub('<br />', result)
	result = re.sub(r'<p>(<br />)*</p>', '', result) # Removes empty paragraphs
	result = re.sub(r'<p>(<br />)*(?P<content>[^\\s].*?)(<br />)*</p>', r'<p>\\g<content></p>', result) # Removes invalid line breaks
	result = emphasisMatcher.sub(r'<em>\\g<word></em>', result)
	result = strongEmphasisMatcher.sub(r'<strong>\\g<word></strong>', result)
	result = namedURLMatcher.sub(r'<a href="\\g<url>">\\g<name></a>', result)
	result = urlMatcher.sub(r'<a href="\\g<url>">\\g<url></a>', result)
	result = emailMatcher.sub(r'<a href="mailto:\\g<email>">\\g<email></a>', result)

	return result

Това е един хубав пример, влизащ в първите пет. Регулярните изрази са точни и резултата е сравнително дуракоустойчив. Програмата прави малко различни неща и лесно се разбира.

Но имат един дефект откъм четимост - именованите групи. Според мен те по-скоро усложняват нещата. Допълнително, цялостната структура - регулярните изрази и заместващите шаблони са отделени - за да ги проследя погледът ми трябва да прескача нагоре-надолу из програмата и трябва да помня имена на променливи и на групи.

1 \*(\w+)\*. Поне на мен [*] ми изглежда странно.

2 http://.*? не е съвсем подходящ регулярен израз за url. Минава заради странния клас след него. Всъщност, може би е по-добре да се пренапише като [(http://\S+) (.*?)]. Плюс съответните имена на групи.

3 Според мен това ще изглежда по-добре като ламбда израз. Въвеждането на ново име ми се вижда ненужно.

…по-близо…

import re, cgi

def header_replace(matchobj):
  match_len = str(len(matchobj.group(1)))
  return '</p><h' + match_len + '>' + matchobj.group(2) + '</h' + match_len + '><p>'

def wiki(text):
  header_match = r'\\s*(=+)\\s*([^=\\015\\012]+?)\\s*\\1\\s*' 4
  paragraph_match = r'([ \\t]*\\015?\\012[ \\t]*){2,}'
  paragraph_replace = r'</p><p>'
  newline_match = r'[ \\t]*\\015?\\012[ \\t]*'
  newline_replace = r'<br />'
  empty_paragraph_match = r'<p>\\s*<\\/p>'
  untitled_url_match = r'((h|H)(t|T)(t|t)(p|P):\\/\\/[^\\s<"]+)' 5
  untitled_url_replace = r'<a href="\\1">\\1</a>'
  titled_url_match = r'\\[<a href="((h|H)(t|T)(t|T)(p|P):\\/\\/[^"]+)">\\1<\\/a>\\s+([^\\]]+)\\]'
  titled_url_replace =  r'<a href="\\1">\\6</a>'
  bold_match = r'([\\s>])\\*(\\S+)\\*([\\s<])'
  bold_replace = r'\\1<strong>\\2</strong>\\3'
  italic_match = r'\\b_(\\S+)_\\b'
  italic_replace = r'<em>\\1</em>'
  mail_match = r'([a-zA-Z0-9\\._\\-]+@([a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?\\.)+[a-zA-Z]+)'
  mail_replace = r'<a href="mailto:\\1">\\1</a>'

  cgi.escape(text)
  text = '<p>' + text + '</p>'
  text = re.sub(header_match, header_replace, text)
  text = re.sub(paragraph_match, paragraph_replace, text)
  text = re.sub(newline_match, newline_replace, text)
  text = re.sub(empty_paragraph_match, '', text)
  text = re.sub(untitled_url_match, untitled_url_replace, text)
  text = re.sub(titled_url_match, titled_url_replace, text)
  text = re.sub(bold_match, bold_replace, text)
  text = re.sub(italic_match, italic_replace, text)
  text = re.sub(mail_match, mail_replace, text)

  return text

Тук програмата е една идея по-добре организирана. Двойките регулярен израз/шаблон за заместване са по-близко, макар tuple да изглеждаше по-приятно. Регулярните изрази са една идея по-слаби от предния пример. Все пак, забелязва се едно упорито повторение на text = re.sub (както и по-горе), което може да бъде избегнато.

4 \012 и \015? Никой не би запомнил, че това са line feed и carriage return. Особено с тези осмични формати. По-добре ползвайте \n и \r.

5 Вместо да пишете (h|H)(t|T)(t|t)(p|P), може (1) да добавите re.I за целия регулярен израз или (2) да ползвате ((?i)http) за да ползвате флага локално. Впрочем, няма и никаква причина да избягвате наклонената черта http:// е достатъчно. http:\/\/ не просто наподобява смътно емблемата Volkswagen - приличина на недомислено взет отнякъде Perl. И да, забелязахме, че някой си е спестил един Shift набирайки този код.

…и победителят е:

def wiki(text):

    text = text.strip()

    strong = (re.compile(r"\\*([\\wа-яА-Я]+?)\\*"), r"<strong>\\1</strong>")
    em = (re.compile(r"_([\\wа-яА-Я]+?)_"), r"<em>\\1</em>")
    url = (re.compile(r"(?<!\\[)(http://[^\\s\\n]+)"), r'<a href="\\1">\\1</a>')   6
    nurl = (re.compile(r"\\[(http://[^\\s\\n]+)\\s(.+)\\]"), r'<a href="\\1">\\2</a>')
    email = (re.compile(r"(.+@[^\\.].*\\.[a-z]{2,})"), r'<a href="mailto:\\1">\\1</a>') 7
    heading = (re.compile(r"\\s*(=+)\\s*(.+?[^\\s])\\s*\\1\\s*\\n+"),             8
                lambda m: m.expand("".join([r"</p><h", str(m.group(1).count("=")), r">\\2", r"</h",
                                            str(m.group(1).count("=")), r"><p>"])))
    sline = (re.compile(r"(?<=.)\\n(?!\\n)"), r"<br/>")   9
    paragraph = (re.compile(r"^(.+)$", re.M), r"<p>\\1</p>")
    empty_p = (re.compile(r"<p></p>"), r"")

    for regex, repl in [strong, em, url, nurl, email, heading, sline, paragraph, empty_p]:
        text = regex.sub(repl, text)

    return text

Кодът тук е почти перфектен. Почти всичко излишно е премахнато - най-вече колонката text = re.sub. За нещастие, регулярните изрази са доста неточни и не отговарят съвсем на условието на задачата.

6 Класът [^\s\n] е съвсем еквивалентен с [^\s], който пък се записва като \S.

7 Това е ужасно грешно. Взема всичко на същия ред преди @. Много ме е яд, че го срещам в тази програма.

8 Тези четири регулярни израза просто се разминават с условието.

9 Нещо, което малко хора знаят: в SGML (родителя на HTML) синтаксиса <tag/foo/> е съкратен запис за <tag>foo</tag>. Съответно, <br/> не се чете много добре от някои обратно-съвместими браузъри. Вместо това се препоръчва да се пише <br />.

Bottom line

Ако сте внимавали в картинката, вероятно сте забелязали, че и трите решения са малко или повече неточни. Всъщност, някои дори не получават пълния брой точки. Избрах да ги покажа защото исках да илюстрирам цялостната структура на програмата, не магии с регулярни изрази. Пък и показват, че задачата все пак може да се реши единствено с регулярни изрази.

Все пак забелязвам, че доста от предалите правят грешки с регулярните изрази. Горещо ви препоръчвам да станете майстори в тази сфера. Регулярните изрази са безспорно най-силния инструмент когато става въпрос за обработка на текст. И бързо решават много, много проблеми. Напоследък рядко минава работен ден, в който не ги ползвам. Това умение ще ви много е полезно, независимо на какъв език програмирате.

Моето решение (с преписване)

Видях няколко хубави идеи в горните три примера. Ето до какво стигнах като опитах да ги комбинирам в едно:

def wiki(text):
    patterns = [
        ('\\r\\n', '\\n'), 10
        ('\\r', '\\n'),
        (r'(?<=\\W)\\*(\\w+)\\*(?=\\W)', r'<strong>\\1</strong>'),
        (r'\\b_(\\w+)_\\b', r'<em>\\1</em>'),
        (r'\\[(http://\\S+) (.*?)\\]', r'<a href="\\1">\\2</a>'),
        (r'(?<!<a href=")(http://\\S+)(?!">)', r'<a href="\\1">\\1</a>'), 11
        (r'[\\w_+.]+@([\\w_+.]+\\.)+\\w+', r'<a href="mailto:\\1">\\1</a>'), 12
        (r'^(=+)(.*)\\1$', lambda _: "</p><h%s>%s</h%s><p>" % (len(_.group(1)), _.group(2).strip(), len(_.group(1)))), 13
        (r'\\n{2,}', '</p><p>'),
        (r'\\n', '<br />'),
        (r'<p>(\\s*<br />)+\\s*', '<p>'),
        (r'(<br />\\s*)+</p>', '</p>'),
        (r'<p>\\s*</p>', ''),
    ]   

    return reduce(
        lambda result, repl: re.sub(re.compile(repl[0], re.U | re.M), repl[1], result), 14
        patterns, "<p>%s</p>" % text.strip())

Силно базирам идеите си на третия пример. Драстичната разлика е че не записвам n-орките в променливи, ами директно конструирам списък от тях. Смятам, че това е предимство. В трети пример се налага да се поддържат два списъка - един от променливи и един, който се итерира във for. Самите имена на променливи също усложняват - биха могли да бъдат използвани навсякъде (например в друг регулярен израз) и трябва добре да прегледам функцията преди да пипам.

Променливите от третия пример имаха едно предимство - казваха за какво служи всеки израз (документация). Въпреки това, смятам, че регулярните изрази са достатъчно очевидни - може само с бърз поглед да прецените, че този служи за заглавия а онзи за електронна поща. Ако все пак държите да ги наименовам, бих го нарпавил в коментар накрая на реда - така кодът остава по-прост.

10 Нормализирам новите редове. След тези два регулярни израза, всичко ще бъде с linux-ски нов ред.

11 Тук играя на по-сигурно. Като страничен ефект, ако някой напише адрес в кавички (“тя каза „http://fmi.py-bg.net е адресът, който ти трябва“") ще проработи правилно.

12 Гледайки решенията, виждам че на много хора им убягва какво прави \w. Еквивалентен е на [a-zA-Z0-9_]. Ако ползвате re.U, ще хваща и кирилица.

13 Няма да се уморя да го повтарям. Според мен _ в този пример носи точно толкова информация, колкото и m в третия пример. Но се откроява особено добре.

14 Всъщност, само исках да се изфукам, че умея да ползвам reduce(). И да ви напрегна мозъците. Един for подобно на третото решение щеше да е по-елегантен тук.

Кой взе точките?

И накрая идваме до важния въпрос - кой получи бонус точки за елегантни решения. От началото на курса се убедихме в две неща за вас - обичате точките и обичате предизвикателствата. Нека да ги комбинираме в едно.

"010c1000100011001111100c00001c1a1ad3aaa3aae330a13333" +
"aadaaaa3d1b5d39bbb9bbe880b58898bb3bdbd935da34917d81f" +
"9bc21acb9b1d4731384a99013e09bee7f2ad90f3fd9100d0b19"

Това е един текстов низ, съдържащ резултатите. Как го произведох?

  1. Създадох списък от факултетните ви номера. Всеки номер се съдържа толкова пъти, колкото точки има.
  2. Разбърках списка произволно
  3. Записах всеки факултетен номер в шестнайсетична система, като добавих нули в началото така че да се получи петбуквен текстов низ. Създадох нов списък от тези записи.
  4. Обхождам списъка пет пъти, извеждайки първо първата буква на всеки факултетен номер, после втората, после третата и т.н.

Например, ако представете си че резултатите са 699324 - 3, 00274 - 2, 1048575 - 1. Първо си правя списък [699324, 699324, 699324, 4386, 4386, 1048575]. Обръщам го до шестнайсетични числа и получавам ['aabbc', 'aabbc', 'aabbc', '01122', '01122', 'fffff']. Правя и едно разбъркване, но да кажем, че то върне същия низ. Тръгвайки по последната стъпка получавам.

aaa00faaa11fbbb11fbbb22fccc22f

Вярвам знаете достатъчно добре Python за да го използвате за да отговорите на въпроса дали сте получили бонус точки. Моята програма която го прави е точно 103 символа, събрани на един ред (за петцифрен факултетен номер). Ще ви дам две подсказки. Първо, ако сте направили декриптирането успешно, ще забележите че факултетен номер 42 има една точка. Второ, някои думички от моя код за проверка на точките са int, join, zip, re.findall, len и count, изписани в този ред.

Чувствайте се поканени да споделите вашите решения на решението на трета задача :) .