Разбор задачи
Я не удержусь, и добавлю несколько комментариев / нравоучений по поводу этого примера...
Про etree:
Мне помнится, что из вас почти никто так и не пытался использовать etree, так что прошу вас посмотреть в мою версию, чтобы иметь какое-то представление о том, как оно применяется в жизни. Сравните это с вашими решениями на регулярных выражениях, устыдитесь и проникнетесь тем, мимо какой полезной штуки вы прошли не глядя! (Я не акцентирую ваше внимание на том, что etree сделан в согласии со стандартом XML, и поэтому он никак не зависит от того, где автор какого-нибудь XML-ника решил сделать перенос строки). Попробуйте хотя бы посчитать число строк, которое заняло решение какой-то подзадачки у вас на регулярных выражениях, и сколько строк потребовалось здесь!
Дабы проще было разбираться, попробуйте отследить путь переменных:
GUI.xml – это разобранное в etree дерево всего файла;
GUI.xmls[sentence] – это словарь, в котором ключ – текст предложения, значения – XML-дерево для одного предложения;
Tree.sentence – это XML-дерево для одного пердложения.
Про преобразование деревьев:
В ваших решениях я наблюдал несколько подходов к решению задачи: как нам перевести деревья из представления, где у каждого узла известен родитель (в котором они в syntagrus), к представлению, в котором у каждого родителя известны дети (которое мы умеем рисовать). Большинство подходов сводились, в конечном счёте, к следующему: мы создаём родителя, а потом проходим по списку всех известных вершин дерева, и если у кого-нибудь данный узел указывался в качестве родителя, то добавляли его в качестве ребёнка. Собственно, мой подход здесь ничем не отличается.
Это решение работает за квадратичное время от количества вершин, но так как предложений даже с сотней слов уже почти не бывает (а 100^2 = 10000, что не очень много), то такое время работы вполне приемлемо.
Эту задачу можно решать и за линейное время, совсем просто. Предположим, parents – словарь, в котором по ключу узлу можно получить значение родителя узла. Мы хотим сделать словарь children, в котором по ключу родителю лежит список детей:
Доска почёта: Аня Выборнова и Егор Лакомкин изобрели такой же подход.
Про рисование интерфейса:
В лице класса GUI и того, как он запускается, я демонстрирую здесь довольно общепринятый сейчас подход к тому, как делать интерфейсы (что графические, что вебовские – впрочем, в flask оно выглядит проще). А именно: мы заводим класс (здесь GUI), в котором есть один-несколько методов для рисования (здесь это create_ui*), несколько методов, которые обрабатывают события (здесь это open, search, redraw), и метод run, запуск которого и есть запуск всей программы.
Про всякое рисование окошек я здесь показал почти все возможности, которые есть в Tkinter, надеюсь, этот код не выглядит страшным.
Про оформление кода:
Хорошим тоном считается:
- чтобы перед каждым определемнием функции или класса оставлять по пустой строке (последование придерживание такой идеи для большинства из вас не характерно);
- чтобы название переменной или функции было достаточно информативным и не требовало комментариев (здесь и ко мне есть нарекания; и здесь и у большинства из вас всё хорошо);
- чтобы все константы были вынесены наверх и названы содержательным образом;
- чтобы код функции всегда укладывался в десять строк (эта константа специфична для питона; для других языков тут правила другие);
- чтобы функция, которой предполагается пользоваться только другим функциям того же класса/модуля, начинались с подчёркивания;
- чтобы всё, что касается одной темы, было рядом, а всё, что касается разных тем, было далеко
- чтобы не было слишком длинных строк (длиннее 70-80 букв).
Ещё хорошим тоном считается иметь гораздо больше договорённостей с собой о том, как делать мелочи (чтобы одинаковые вещи всегда делались одинаково) и соблюдать их. Думаю, моё решение вполне можно воспринимать и как пример применения этих идей к жизни. Не нужно стремиться к тому, чтобы ваш код был похож на мой, но я очень рекомендую, чтобы вы старались делать код столь же упорядоченным. Это упрощает чтение кода – и в первую очередь, вам же самим во время отладки.
Код программы
1 # coding: utf-8
2 import Tkinter
3 import tkFileDialog
4 from xml.etree import ElementTree
5
6 class Tree(object):
7 dx = 25
8 dy = 25
9 font = 'Arial'
10
11 def __init__(self, canvas, sentence, id=None):
12 if id is None:
13 self.word = sentence.find('.//*[@DOM="_root"]')
14 id = self.word.attrib['ID']
15 else:
16 self.word = sentence.find('.//*[@ID="{0}"]'.format(id))
17 self.canvas = canvas
18 self.sentence = sentence
19 self.children = [Tree(canvas, sentence, word.attrib['ID'])
20 for word in sentence.findall('.//*[@DOM="{0}"]'.format(id))]
21
22 def draw(self):
23 self._draw_node(self.dx, self.dy)
24 self._row = [self]
25 while self._row:
26 self._draw_row()
27 self._next_row()
28
29 def _draw_row(self):
30 max_x = self.dx
31 for parent in self._row:
32 for child in parent.children:
33 child._draw_node(max_x, parent.y1 + self.dy)
34 max_x = child.x1 + self.dx
35 parent._draw_line(child)
36
37 def _draw_node(self, x, y):
38 self.canvas_id = self.canvas.create_text(
39 (x, y),
40 text=self.word.text,
41 anchor='nw',
42 justify='center',
43 font=self.font,
44 )
45 self.x0, self.y0, self.x1, self.y1 = self.canvas.bbox(self.canvas_id)
46
47 def _draw_line(self, child):
48 x0, y0 = (self.x0 + self.x1) / 2, self.y1
49 x1, y1 = (child.x0 + child.x1) / 2, child.y0
50 self.canvas.create_line((x0,y0), (x1,y1), arrow='last')
51
52 def _next_row(self):
53 result = []
54 for parent in self._row:
55 for child in parent.children:
56 result.append(child)
57 self._row = result
58
59 class GUI(object):
60 W, H = 500, 500
61
62 def __init__(self):
63 self.xmls = {}
64 self.xml = ElementTree.fromstring("<xml/>")
65 self.create_ui()
66
67 def create_ui(self):
68 self.root = Tkinter.Tk()
69 self.create_ui_top()
70 self.create_ui_middle()
71 self.create_ui_bottom()
72
73 def create_ui_top(self):
74 top = Tkinter.Frame(self.root)
75 self.query = query = Tkinter.Entry(top)
76 search = Tkinter.Button(top, text="Search", command=self.search)
77 openfile = Tkinter.Button(top, text="Open file", command=self.open)
78 query.pack(side="left", fill="both", expand="yes")
79 search.pack(side="left")
80 openfile.pack(side="left")
81 top.pack(side="top", fill="x")
82 query.bind("<Return>", self.search)
83
84 def create_ui_middle(self):
85 middle = Tkinter.Frame(self.root)
86 self.sentences = sentences = Tkinter.Listbox(middle)
87 scroll = Tkinter.Scrollbar(middle, command=sentences.yview)
88 sentences['yscrollcommand'] = scroll.set
89 sentences.pack(side="left", fill="both", expand="yes")
90 scroll.pack(side="left", fill="y")
91 middle.pack(side="top", fill="both", expand="yes")
92 sentences.bind("<<ListboxSelect>>", self.redraw)
93
94 def create_ui_bottom(self):
95 self.canvas = canvas = Tkinter.Canvas(self.root,
96 width=self.W, height=self.H, background="white")
97 canvas.pack(side="top", fill="both", expand="yes")
98
99 def open(self):
100 file = tkFileDialog.askopenfile()
101 self.xml = ElementTree.parse(file).getroot()
102
103 def search(self, ev=None):
104 query = self.query.get()
105 self.xmls = {}
106 self.sentences.delete(0, "end")
107 xml_sentences = self.xml.findall(u'.//*[@LEMMA="{0}"]/..'.format(query.upper()))
108 for xml_sentence in xml_sentences:
109 sentence = "".join(word.text + word.tail for word in xml_sentence)
110 sentence = "".join(xml_sentence.itertext())
111 sentence = sentence.replace("\n", "")
112 self.xmls[sentence] = xml_sentence
113 self.sentences.insert("end", sentence)
114 if not xml_sentences:
115 self.sentences.insert("end", "nothing found")
116
117 def redraw(self, event):
118 self.canvas.delete("all")
119 sentence = self.sentences.get("active")
120 xml_sentence = self.xmls[sentence]
121 Tree(self.canvas, xml_sentence).draw()
122
123 def run(self):
124 return self.root.mainloop()
125
126 if __name__ == "__main__":
127 GUI().run()