PyUNO — быстрое незначительное редактирование xls-отчета из Python

    Просто и быстро


    Не так давно я столкнулся с необходимостью запротоколировать список изменений в нашем ПО. Заказчик прислал мне формуляр, который я должен был заполнить в соответствии с их внутренними требованиями к документации. Я открыл прилагавшийся к письму файл «Изменения 1.xls» и немного приуныл. Точнее, мне в голову последовательно пришли мысли об увольнении, а затем — о самоубийстве. Формуляр состоял из 14 колонок. Быстро перемножив в уме количество колонок с числом внесенных нами атомарных изменений (около пятисот), я пошел курить.
    Сделать такую работу руками мне не под силу. Большинство данных (номера новых версий, описания изменений и т. п.) у меня, конечно, имелись в наличии, но в разных местах и самых причудливых форматах. Но семьсот копипастов — увольте. Поэтому мне пришлось немного освоить PyUNO. На всякий случай — опишу вкратце процесс управления документом OOo из питоновского биндинга, вдруг кому пригодится.

    Нам потребуются сам Open Office, python и, собственно, биндинги:
    $ sudo yum search pyuno ure
    

    Запускаем OOo (нам нужны электронные таблицы, но метод работает для всех приложений — см. пример 2) с включенной опцией «слушать сокет»:
    $ oocalc "-accept=socket,host=localhost,port=8100;urp;" &
    $ soffice "-accept=socket,host=localhost,port=8100;urp;" -writer -headless &
    

    …и отправляемся писать код. Для начала — получим инстанс документа:
    import uno
    from os.path import abspath, isfile, splitext
    
    def getDocument(inputFile) :
      localContext = uno.getComponentContext()
      resolver = localContext.ServiceManager.createInstanceWithContext( \
        "com.sun.star.bridge.UnoUrlResolver", localContext)
      try:
        context = resolver.resolve( \
        "uno:socket,host=localhost,port=%s;urp;StarOffice.ComponentContext" % 8100)
      except NoConnectException:
        raise Exception, "failed to connect to OpenOffice.org on port %s" % 8100
      desktop = context.ServiceManager.createInstanceWithContext( \
        "com.sun.star.frame.Desktop", context)
      document = desktop.loadComponentFromURL( \
        uno.systemPathToFileUrl(abspath(inputFile)), "_blank", 0, tuple([]))
    

    Получение данных для заполнения итогового документа из текстовых файлов и change-логов я оставлю за рамками данной заметки. Пусть они у нас просто появляются в псевдополе data по мановению волшебной палочки. Итак, приступим к заполнению нашего документа (заполняем столбец №2):
    from com.sun.star.beans import PropertyValue
    
    def fillDocument(inputFile, col, data) :
      try:
        sheet = getDocument(inputFile).getSheets().getByIndex(0)
        row = 2
        while True:
          row = row + 1
          val = sheet.getCellByPosition(col, row).getFormula()
          if val != '' :
            sheet.getCellByPosition(col, row).setFormula(val.replace(%VERSION%, data))
          else :
            break;
        '''
        All the rows are now filled, It's time to save our modified document
        '''
        props = []
        prop = PropertyValue()
        prop.Name = "FilterName"
        prop.Value = "MS Excel 97"
        props.append(prop)
        document.storeToURL(uno.systemPathToFileUrl(abspath(inputFile)) + ".out.xls", tuple(props))
      finally:
        document.close(True)
    

    Точно так же можно обойтись с любым другим столбцом. Можно даже в дороге сходить в Google Translate за переводом.

    Еще некоторые полезные возможности


    Вставка другого документа


    '''
    Required for Ctrl+G :-)
    '''
    from com.sun.star.style.BreakType import PAGE_BEFORE, PAGE_AFTER
    
    def addAtTheEnd(inputFile) :
      cursor.gotoEnd(False)
      cursor.BreakType = PAGE_BEFORE
    
      cursor.insertDocumentFromURL(uno.systemPathToFileUrl(abspath(inputFile)), ())
    

    Поиск и замена


    def findAndReplace(pattern, substTo, replaceAll, caseSensitive) :
      search = document.createSearchDescriptor()
      search.SearchRegularExpression = True
      search.SearchString = pattern
      search.SearchCaseSensitive = caseSensitive
    
      result = document.findFirst(search)
      while found:
        result.String = string.replace(result.String, pattern, substTo)
        if not replaceAll :
          break
        result = document.findNext(result.End, pattern)
    

    Экспорт в PDF


    def exportToPDF(outputFile)
      props = []
      prop = PropertyValue()
      prop.Name = "FilterName"
      prop.Value = "writer_pdf_Export"
      props.append(prop)
      document.storeToURL(uno.systemPathToFileUrl(abspath(outputFile)), tuple(props))
    

    Disclaimer


    Сразу хочу оговориться: код претендует на звание наколеночного говнокода, выполняемого один раз. Но в качестве «ну-ка быстро обновим файл отчета» — мне лично сэкономил уже кучу времени.

    — Вот тут и вокруг можно собрать еще какие-то обрывки информации: http://wiki.services.openoffice.org/wiki/Uno/FAQ
    — Шаблонизатор на питоне для OOo: appyframework.org
    — То же самое, только для C++: habrahabr.ru/blogs/cpp/116228

    Upd: В комментариях подсказывают другой способ: xlwt.
    Upd2: Для генерации в более «новомодный» формат xlsx, есть такая полезная либа: xlsx.dowski.com.
    За оба дополнения — огромная благодарность tanenn.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 20

      +1
      Я понимаю, что я давно от жизни отстал, но для Windows описание будет? Или на документацию как запустить на оной, ссылку скиньте, пожалуйста.
      0
      Сейчас актуальна работа с LibreOffice :)
      Хотя отличаться будет, наверно, только именами сервисов.
        0
        Разумеется, да.
      • UFO just landed and posted this here
        • UFO just landed and posted this here
            +1
            Угу, спасибо. Не знал.
            В самой заметке добавил ссылку.

            А он умеет читать/изменять готовые файлы?
            • UFO just landed and posted this here
                0
                Да, спасибо огромное еще раз. Я почему-то сразу стал искать биндинги, когда мне потребовалось. У меня не было никаких серверных задач.

                Но этот xlwt выглядит явно гораздо более разумным решением. Он, небось, напрямую с xml — через схемы преобразования — шпарит. Судя по «необходимо открыть файл, создать копию в памяти, дописывать в него, и уже его сохранять».
                • UFO just landed and posted this here
                    0
                    Угу, еще раз спасибо, обновил заметку.
          0
          Было дело я тоже колдовал с экселем под линукс. Правда, остановился на xlwt и дописал поверх него утилитку для более простого построения. Может будет кому полезно: github.com/lightcaster/xlrep
            0
            Код с заменой нерабочий. Вот накатал скриптик, в котором всё исправлено:

            #!/usr/bin/python
            # -*- coding: utf-8 -*-

            import sys
            import uno
            from os.path import abspath, isfile, splitext

            PORT = 8100

            if len(sys.argv) < 4:
            print 'Usage: oreplace SEARCH REPLACE PATH'
            sys.exit(0)

            inputFile = sys.argv[3]
            searchString = sys.argv[1]
            replaceString = sys.argv[2]

            localCtx = uno.getComponentContext()
            localSmgr = localCtx.ServiceManager
            uresolver = localSmgr.createInstanceWithContext(
            "com.sun.star.bridge.UnoUrlResolver", localCtx)
            ctx = uresolver.resolve( \
            "uno:socket,host=localhost,port=%d;urp;StarOffice.ComponentContext" % PORT)
            smgr = ctx.ServiceManager
            desktop = smgr.createInstanceWithContext(
            "com.sun.star.frame.Desktop", ctx)

            document = desktop.loadComponentFromURL( \
            uno.systemPathToFileUrl(abspath(inputFile)), "_blank", 0, tuple([]))

            doc = document

            if hasattr(document, 'getSheets'):
            sheets = document.getSheets()
            doc = sheets.getCellRangesByName(u'A1:AMJ1048576')[0]

            if not hasattr(doc, 'createReplaceDescriptor'):
            print 'Unknown document type'
            sys.exit(1)

            try:
            replaceDesc = doc.createReplaceDescriptor()
            replaceDesc.SearchString = searchString
            replaceDesc.ReplaceString = replaceString

            found = doc.replaceAll(replaceDesc)

            document.store()
            finally:
            document.close(True)
              0
              упс… извините за пробелы между строками кода. В предпросмотре всё было нормально. Как это можно исправить?
                0
                Исправить никак, на будущее — в правом верхнем углу редактора комментария есть ссылка «html-теги». Обратите внимание там на третий пункт: строчку «source …».

                Что касается поиска — я специально предложил поиск для обычного документа, не для электронной таблицы. Главное — принцип, дальше каждый уж сам извращается, как умеет.
                  0
                  понятно, что у вас наиболее упрощенный пример, но мне просто понадобился поиск и по таблицам, вот и выложил как есть.
                    0
                    Ага, спасибо, вам уже зачлось в карме.

            Only users with full accounts can post comments. Log in, please.