Программирование на Python 3. Руководство издательство СимволПлюс
Скачать 3.74 Mb.
|
361 Если строка не содержит символ «=», то проверяется – не является ли она маркером начала комментария. В этом случае в переменную narra tive записывается пустая строка. Это означает, что при чтении всех последующих строк условное выражение в первой инструкции if бу дет давать в результате значение True, по меньшей мере, пока не будет прочитан маркер конца комментария. Если ни одно из условий в ветках if и elif не было выполнено, следова тельно, возникла ошибка, поэтому в заключительном предложении else возбуждается исключение KeyError, чтобы обозначить ее. return True except (EnvironmentError, ValueError, KeyError, IncidentError) as err: print("{0}: import error: {1}".format( os.path.basename(sys.argv[0]), err)) return False finally: if fh is not None: fh.close() По окончании чтения всех строк вызывающей программе возвращает ся значение True, если не было возбуждено исключение, – в этом слу чае блок except перехватит исключение, выведет для пользователя со общение об ошибке и вернет False. И в заключение, независимо от про исходящего, файл будет закрыт. Синтаксический анализ текста с помощью регулярных выражений Читателям, не знакомым с регулярными выражениями, рекомендует ся прочитать главу 12, прежде чем приступать к чтению этого раздела, или сразу перейти к чтению следующего раздела (стр. 364) и вернуть ся сюда позднее. Использование регулярных выражений для разбора текста часто дает более короткий программный код по сравнению с тем, где все действия по разбору выполняются вручную, как это делалось в предыдущем подразделе, но в нем сложнее реализовать вывод ясных сообщений об ошибках. Ниже приводится программный код метода import_text_re gex() , который мы рассмотрим в два приема. Сначала мы обсудим ре гулярные выражения, а затем реализацию синтаксического анализа, но опустим блоки except и finally, поскольку в них не появилось ниче го нового для нас. def import_text_regex(self, filename): incident_re = re.compile( r"\[(?P r"^\.NARRATIVE_START\.$(?P r"^\.NARRATIVE_END\.$", re.DOTALL|re.MULTILINE) 362 Глава 7. Работа с файлами key_value_re = re.compile(r"^\s*(?P r"(?P Регулярные выражения записаны как «сырые» (raw) строки. Это устраняет необходимость дублировать каж дый символ обратного слеша (вместо \ записывать \\); например, если не использовать «сырые» строки, второе регулярное выражение пришлось бы записать как: " ^\\s*(?P . В этой книге для записи регулярных выражений мы всегда бу дем использовать «сырые» строки. Первое регулярное выражение, incident_re, используется для захвата всей записи с информацией об инциденте. При таком подходе любой посторонний текст между записями останется незамеченным. Данное регулярное выражение в действительности состоит из двух частей. Первая часть \[(?P лу «[», затем соответствует, с захватом в группу id, произвольному числу символов, отличных от «]», затем соответствует символу «]» (что дает нам идентификатор отчета) и затем соответствует любому числу (но не менее одного) любых символов (включая символы перево да строки, благодаря флагу re.DOTALL), захватывая их в группу keyval ues . Символы, включенные в группу keyvalues, являются необходимым минимумом, чтобы перейти ко второй части регулярного выражения. Вторая часть первого регулярного выражения: ^\.NARRATIVE_START\.$ (?P . Она соответствует точному тек сту .NARRATIVE_START., затем произвольному числу символов, которые захватываются в группу narrative, и затем точному тексту .NARRATI VE_END. в конце записи с информацией об инциденте. Флаг re.MULTILINE означает, что в данном регулярном выражении символ ^ соответствует началу каждой строки в файле (а не началу всей строки), а символ $ со ответствует концу каждой строки в файле (а не концу всей строки), по этому соответствие маркерам начала и конца комментария будет обна руживаться, только если они находятся в начале строки. Второе регулярное выражение key_value_re используется для захвата строк ключ=значение и соответствует началу каждой строки в задан ном тексте, произвольному (в том числе и нулевое) числу последую щих пробельных символов, за которыми следуют символы, отличные от символа «=», захватываемые в группу key, последующему символу «=» и всем остальным символам в строке текста (исключая начальные и завершающие пробельные символы), захватываемым в группу value. Основная логика синтаксического анализа файла осталась той же, что использовалась для анализа текста вручную и описана в предыдущем подразделе, только на этот раз сама запись и информация об инциден те извлекаются не посредством построчного чтения содержимого фай ла, а с помощью регулярных выражений. «Сырые» строки, стр. 85 Запись и синтаксический анализ текстовых файлов 363 fh = None try: fh = open(filename, encoding="utf8") self.clear() for incident_match in incident_re.finditer(fh.read()): data = {} data["report_id"] = incident_match.group("id") data["narrative"] = textwrap.dedent( incident_match.group("narrative")).strip() keyvalues = incident_match.group("keyvalues") for match in key_value_re.finditer(keyvalues): data[match.group("key")] = match.group("value") data["date"] = datetime.datetime.strptime( data["date"], "%Y%m%d").date() data["pilot_percent_hours_on_type"] = ( float(data["pilot_percent_hours_on_type"])) data["pilot_total_hours"] = int( data["pilot_total_hours"]) data["midair"] = bool(int(data["midair"])) if len(data) != 9: raise IncidentError("missing data") incident = Incident(**data) self[incident.report_id] = incident return True Метод re.finditer() возвращает итератор, который поочередно возвра щает неперекрывающиеся совпадения. В начале цикла создается сло варь data для хранения информации об инциденте, как и раньше, но на этот раз идентификатор отчета и текст комментария извлекаются не посредственно из найденного соответствия регулярному выражению incident_re . Затем из группы keyvalues извлекаются сразу все строки ключ=значение и к ним применяется метод re.finditer() регулярного выражения key_value_re, чтобы выполнить обход отдельных строк ключ=значение . Каждая найденная пара (ключ, значение) помещает ся в словарь data, поэтому все значения сохраняются в виде строк. Да лее те значения, которые не должны быть строками, замещаются зна чениями соответствующих типов, для чего выполняются те же преоб разования строк, что применялись при разборе текста вручную. Мы добавили проверку, чтобы убедиться, что словарь data содержит ровно девять элементов данных, потому что в случае повреждения за писи с информацией об инциденте итератор key_value.finditer() может отыскать слишком много или слишком мало строк ключ=значение. Оканчивается метод точно так же, как и раньше, – создается новый объект Incident, который затем помещается в словарь инцидентов, по сле чего вызывающей программе возвращается значение True. Если чтото пойдет не так, блок except выведет соответствующее сообщение об ошибке и вернет False, а блок finally закроет файл. 364 Глава 7. Работа с файлами Одной из особенностей, которые делают программный код, анализи рующий текст вручную или с применением регулярных выражений, таким коротким и таким простым, является механизм обработки ис ключений языка Python. Программный код не проверяет результаты преобразований строк в даты, числа или логические значения, и в нем отсутствуют проверки попадания значений в допустимые границы (это делает класс Incident). Если какаялибо из этих операций завер шится неудачей, интерпретатор возбудит исключение, и мы преду сматриваем обработку всех исключений в одном месте, в конце мето дов. Другое преимущество использования механизма исключений пе ред явной проверкой на наличие ошибок состоит в хорошей масштаби руемости программного кода – даже в случае изменения формата записи и увеличения количества элементов данных программный код, выполняющий обработку ошибок, не будет увеличиваться в объеме. Запись и синтаксический анализ файлов XML Некоторые программы используют файлы формата XML для хранения всех обрабатываемых данных, другие обеспечивают только возмож ность импорта/экспорта в формате XML. Способность импортировать и экспортировать данные в формате XML не будет лишней, и поддерж ку этого формата всегда стоит предусматривать, даже если основным форматом, с которым работает программа, является текстовый или двоичный формат. Язык Python предоставляет три способа записи файлов в формате XML: вручную, посредством создания дерева элементов и использования его метода write(), а также посредством создания DOM и использования его метода write(). Для чтения и анализа файлов XML используется четыре способа: чтение и разбор файла XML вручную (не рекомендует ся и не рассматривается в этой книге, поскольку может оказаться чрезвычайно сложно корректно обработать некоторые из наиболее ту манных и расширенных возможностей) или с использованием парсе ров ElementTree, DOM или SAX. Формат XML записи с информацией об авиационном инциденте при водится на рис. 7.3. В этом разделе будет показано, как выполнять за пись в этом формате вручную и как выполнять запись с помощью дере ва элементов и DOM, а также как читать и анализировать этот формат с помощью парсеров ElementTree, DOM и SAX. Если вас не интересует вопрос выбора способа чтения или записи файлов в формате XML, вы можете просто прочитать подраздел «Деревья элементов», следующий ниже, и затем перейти к заключительному разделу главы «Произволь ный доступ к двоичным данным в файлах» (стр. 376). Запись и синтаксический анализ файлов XML 365 Деревья элементов Запись данных с использованием дерева элементов выполняется в два этапа: сначала должно быть создано дерево элементов, представляю щее данные, и затем дерево должно быть записано в файл. Некоторые программы могут использовать дерево элементов в качестве основной структуры представления своих данных – в этом случае дерево эле ментов уже имеется изначально и остается лишь записать его в файл. Мы рассмотрим метод export_xml_etree(), разделив его на две части: def export_xml_etree(self, filename): root = xml.etree.ElementTree.Element("incidents") for incident in self.values(): element = xml.etree.ElementTree.Element("incident", report_id=incident.report_id, date=incident.date.isoformat(), aircraft_id=incident.aircraft_id, aircraft_type=incident.aircraft_type, pilot_percent_hours_on_type=str( incident.pilot_percent_hours_on_type), pilot_total_hours=str(incident.pilot_total_hours), midair=str(int(incident.midair))) airport = xml.etree.ElementTree.SubElement(element, "airport") airport.text = incident.airport.strip() narrative = xml.etree.ElementTree.SubElement(element, pilot_percent_hours_on_type="9.09090909091" pilot_total_hours="440" midair="0"> ON A GO AROUND FROM A NIGHT CROSSWIND LANDING ATTEMPT THE AIRCRAFT HIT A RUNWAY EDGE LIGHT DAMAGING ONE PROPELLER. : Рис. 7.3. Пример записи с информацией об авиационном инциденте в формате XML 366 Глава 7. Работа с файлами "narrative") narrative.text = incident.narrative.strip() root.append(element) tree = xml.etree.ElementTree.ElementTree(root) Метод начинается с создания корневого элемента ( цидентах. Для каждой записи создается свой элемент ( в котором будут храниться данные об инциденте, а именованные аргу менты определяют атрибуты элемента. Все атрибуты должны иметь текстовый формат, поэтому даты, числа и логические значения преоб разуются соответствующим образом. Нам не нужно беспокоиться об экранировании символов «&», «<» и «>» (или о кавычках в значениях атрибутов), так как модуль парсера дерева элементов (а также модули парсеров DOM и SAX) делает это автоматически. Каждый элемент ние аэропорта, а второй – текст комментария. При создании подэле мента мы должны указать родительский элемент и имя тега. Для хра нения текста используется атрибут text элемента, доступный для чте ния и записи. После создания элемента ментами ( держащих все записи с информацией об инцидентах, которая затем тривиально просто преобразуется в дерево элементов. try: tree.write(filename, "UTF8") except EnvironmentError as err: print("{0}: import error: {1}".format( os.path.basename(sys.argv[0]), err)) return False return True Запись целого дерева элементов с данными в формате XML выполняет ся простым вызовом его метода, выполняющим запись в указанный файл с использованием указанной кодировки символов. До сих пор практически всякий раз, когда мы указывали кодировку, мы использовали строку "utf8". Она является вполне допустимой для встроенной функции open(), которая может принимать широкий диа пазон кодировок с различными версиями их названий, такими как «UTF8», «UTF8», «utf8» и «utf8». Но в файлах XML могут использо ваться только официальные названия кодировок, по этой причине на звание "utf8" нельзя использовать, и мы используем название "UTF8". 1 1 Дополнительную информацию о названиях кодировок вы найдете на сай тах www.w3.org/TR/2006/RECxml1120060816/#NTEncodingDecl и www. iana.org/assignments/charactersets Запись и синтаксический анализ файлов XML 367 Чтение файла XML с использованием дерева элементов выполняется ничуть не сложнее, чем запись. Запись также выполняется в два эта па: на первом этапе выполняется чтение и анализ содержимого файла XML, а затем производится обход дерева элементов и заполнение сло варя с информацией об инцидентах. Как и прежде, второй этап не яв ляется обязательным, если для хранения данных в памяти использу ется само дерево элементов. Ниже приводится программный код мето да import_xml_etree(), разбитый на две части: def import_xml_etree(self, filename): try: tree = xml.etree.ElementTree.parse(filename) except (EnvironmentError, xml.parsers.expat.ExpatError) as err: print("{0}: import error: {1}".format( os.path.basename(sys.argv[0]), err)) return False По умолчанию парсер дерева элементов использует парсер expat, имен но поэтому мы должны быть готовы перехватывать исключения парсе ра expat. self.clear() for element in tree.findall("incident"): try: data = {} for attribute in ("report_id", "date", "aircraft_id", "aircraft_type", "pilot_percent_hours_on_type", "pilot_total_hours", "midair"): data[attribute] = element.get(attribute) data["date"] = datetime.datetime.strptime( data["date"], "%Y%m%d").date() data["pilot_percent_hours_on_type"] = ( float(data["pilot_percent_hours_on_type"])) data["pilot_total_hours"] = int( data["pilot_total_hours"]) data["midair"] = bool(int(data["midair"])) data["airport"] = element.find("airport").text.strip() narrative = element.find("narrative").text data["narrative"] = (narrative.strip() if narrative is not None else "") incident = Incident(**data) self[incident.report_id] = incident except (ValueError, LookupError, IncidentError) as err: print("{0}: import error: {1}".format( os.path.basename(sys.argv[0]), err)) return False return True 368 Глава 7. Работа с файлами Получив дерево элементов, можно приступать к выполнению итера ций по всем элементам ElementTree.findall() . Информация о каждом инциденте возвращается в виде объекта xml.etree.Element. Здесь используется та же методика обработки атрибутов элемента, что и в разделе с описанием метода import_text_regex() , – сначала все значения сохраняются в словаре da ta , а затем выполняется преобразование таких данных, как даты, чис ла и логические значения, в соответствующие типы данных. Для из влечения элементов нированных последовательностей XML, потому что они автоматиче ски преобразуются в соответствующие символы. При использовании парсеров XML для обработки данных об авиацион ных инцидентах, как и любых других парсеров, будут возбуждаться исключения: в случае отсутствия элементов с названием аэропорта или с комментарием, в случае ошибки при выполнении какоголибо преобразования или при выходе любого числового значения за грани цы допустимого диапазона – этим гарантируется, что ошибочные дан ные будут приводить к прекращению анализа файла и к выводу сооб щения об ошибке. Программный код в конце метода, создающий и со храняющий инциденты, а также программный код обработки исклю чений остался тем же, что мы уже видели ранее. |