понедельник, 23 июля 2012 г.

Парсинг XML и создание PDF Invoice при помощи Python (Перевод)


Обратите внимание: Этот пост был первоначально опубликован на Dzone. Я изменил заголовок, поскольку я уже писал несколько XML парсеров и не хочу запутать моих читателей.
Одна из задач, с которыми я часто сталкиваюсь в своей работе - это взять данные в одном формате, обработать их и создать отчёт или какой-то другой документ. Сегодня мы посмотрим на то, как взять XML ввод, распарсить его при помощи Python и затем создать отчёт при помощи Reportlab - стороннем пакете для Python. Давайте предположим, что моя компания получила заказ на три предмета, который я должен выполнить. Такой XML может выглядеть так:

<?xml version="1.0"?>
<invoice>
    <order_number>456789</order_number>
 <customer_id>789654</customer_id>
 <address1>John Doe</address1>
 <address2>123 Dickens Road</address2>
 <address3>Johnston, IA 55555</address3>
 <address4/>
 <order_items>
  <item>
   <id>11123</id>
   <name>Expo Dry Erase Pen</name>
   <price>1.99</price>
   <quantity>5</quantity>
  </item>
  <item>
   <id>22245</id>
   <name>Cisco IP Phone 7942</name>
   <price>300</price>
   <quantity>1</quantity>
  </item>
  <item>
   <id>33378</id>
   <name>Waste Basket</name>
   <price>9.99</price>
   <quantity>1</quantity>
  </item>
 </order_items>
</invoice>
Сохраним его как order.xml. Теперь мне надо написать парсер и генератор PDF. Вы можете использовать встроенные библиотеки для парсинга XML: SAX, minidom или ElementTree; или же Вы можете загрузить один из сторонних пакетов для этого. Моим любимым является lxml, который содержит версию ElementTree и очень хороший код под названием “objectify”. Этот код берёт XML и превращает его объект Python c точечной нотацией. Я использую его для парсинга так как он позволяет сделать это легко, просто и понятно. Для создания PDF я буду использовать Reportlab.
Вот простой скрипт, который делает всё, что нам надо:
from decimal import Decimal
from lxml import etree, objectify
 
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import inch, mm
from reportlab.pdfgen import canvas
from reportlab.platypus import Paragraph, Table, TableStyle
 
########################################################################
class PDFOrder(object):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self, xml_file, pdf_file):
        """Constructor"""
        self.xml_file = xml_file
        self.pdf_file = pdf_file
 
        self.xml_obj = self.getXMLObject()
 
    #----------------------------------------------------------------------
    def coord(self, x, y, unit=1):
        """
        # http://stackoverflow.com/questions/4726011/wrap-text-in-a-table-reportlab
        Вспомогательный класс для позиционирования в объектах Canvas
        """
        x, y = x * unit, self.height -  y * unit
        return x, y  
 
    #----------------------------------------------------------------------
    def createPDF(self):
        """
        Создаём PDF на основании XML данных
        """
        self.canvas = canvas.Canvas(self.pdf_file, pagesize=letter)
        width, self.height = letter
        styles = getSampleStyleSheet()
        xml = self.xml_obj
 
        address = """ <font size="9">
        SHIP TO:<br/>
        <br/>
        %s<br/>
        %s<br/>
        %s<br/>
        %s<br/>
        </font>
        """ % (xml.address1, xml.address2, xml.address3, xml.address4)
        p = Paragraph(address, styles["Normal"])
        p.wrapOn(self.canvas, width, self.height)
        p.drawOn(self.canvas, *self.coord(18, 40, mm))
 
        order_number = '<font size="14"><b>Order #%s </b></font>' % xml.order_number
        p = Paragraph(order_number, styles["Normal"])
        p.wrapOn(self.canvas, width, self.height)
        p.drawOn(self.canvas, *self.coord(18, 50, mm))
 
        data = []
        data.append(["Item ID", "Name", "Price", "Quantity", "Total"])
        grand_total = 0
        for item in xml.order_items.iterchildren():
            row = []
            row.append(item.id)
            row.append(item.name)
            row.append(item.price)
            row.append(item.quantity)
            total = Decimal(str(item.price)) * Decimal(str(item.quantity))
            row.append(str(total))
            grand_total += total
            data.append(row)
        data.append(["", "", "", "Grand Total:", grand_total])
        t = Table(data, 1.5 * inch)
        t.setStyle(TableStyle([
            ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
            ('BOX', (0,0), (-1,-1), 0.25, colors.black)
        ]))
        t.wrapOn(self.canvas, width, self.height)
        t.drawOn(self.canvas, *self.coord(18, 85, mm))
 
        txt = "Thank you for your business!"
        p = Paragraph(txt, styles["Normal"])
        p.wrapOn(self.canvas, width, self.height)
        p.drawOn(self.canvas, *self.coord(18, 95, mm))
 
    #----------------------------------------------------------------------
    def getXMLObject(self):
        """
        Открываем XML документ и возвращаем lxml XML документ
        """
        with open(self.xml_file) as f:
            xml = f.read()
        return objectify.fromstring(xml)
 
    #----------------------------------------------------------------------
    def savePDF(self):
        """
        Сохраняем PDF
        """
        self.canvas.save()
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    xml = "order.xml"
    pdf = "letter.pdf"
    doc = PDFOrder(xml, pdf)
    doc.createPDF()
    doc.savePDF()
Вот итоговый PDF: letter.pdf
Давайте потратим немного времени на разбор этого кода. Во-первых, мы импортируем набор разных объектов для настройки нашего окружения и получения нужных компонентов из Reportlab и lxml. Кроме того, я импортирую модуль decimal, так как для финансовых расчётов я хочу более точный инструмент, чем стандартная математическая библиотека Python. Затем я создаю класс PDFOrder, который принимает два аргумента - пути к xml файлу и pdf файлу. При его инициализации мы создаём несколько свойств класса, читаем XML файл и возвращаем XML объект. Метод coord используется для позиционирования flowables в  Reportlab, которые являются динамическими объектами, могут разделяться между страницами и принимать различные стили.
Метод createPDF - сердце нашей программы. Объект canvas используется для создания нашего PDF и "рисования" на нём. Я настроил его на размер letter и получил лист стилей по умолчанию. Затем я создал адрес доставки и разместил его вверху страницы, 18mm слева и 40mm сверху. После этого я создал и разместил номер заказа. Наконец, при помощи итератора, я прошёлся по элементам заказа и разместил их во вложенном списке, который поместил в Reportlab’s Table flowable. И, в завершении, я разместил саму таблицу и задал её стиль. После этого сохранил файл на диск и всё.
Документ создан и у меня есть хороший образец для коллег. На данный момент мне осталось лишь навести лоск на документ, используя различные стили текста (например, bold, italic, размер шрифта) или изменяя немного расположение элементов. Это, обычно, зависит уже от клиента или менеджера, так что буду ждать их отзыва.
Теперь Вы знаете как распарсить XML документ в Python и создать на основе полученных данных PDF.

Исходники

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

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