Python、xlsx、画像、マクロ、メディアアート、埋め込みオブジェクト

ご挨拶、habrosociety。



私はxlsxでのpythonの動作について少し書くことにしました。

はい、私はそれについて多くのことが書かれていることを知っていますが、それにもかかわらず、私の仕事のための既製の解決策を見つけることができませんでした。

完成したxlsファイルに数十個のセルを入力する必要があります。



すべては問題ありませんが、ファイルにはマクロ、画像、メディアアートオブジェクト、埋め込みオブジェクトなどが含まれていました。

一般的なソリューション(openpyxl、xlutils.copy、およびその他のいくつか)を試した後、ファイルが少し損なわれることに気付きました。

そして、タスクはセルのポイント編集に正確に削減されたため、他のすべてはそのままでした。







背景



ある国のある銀行には情報システムがないことが突然判明しました

ローンを決定する。

それは、太古の時代から継承されたExcelファイルに置き換えられます

「多くのエクセルプログラマー」の手に渡りました。



そして、何らかの形でローン申請書を銀行に転送する必要があります。

つまり、このファイルを取得し、データを挿入してメールで送信する必要があります。



「私は最初でも最後でもない」と思い、既成のソリューションを探しに行きました。

すべてがそれほど単純ではないことが判明しました。

openpyxl:

なんらかの理由で、触れなかったセルの輪郭を完全に保持していませんでした(境界線は左下隅にのみ残しました)

keep_vba ​​= Trueを使用している場合でも、「壊れた」xlsmファイル(マクロを含むことができます)を取得しました。

xlutils:

写真とメディアアートオブジェクトを失い、xlsmではなくxlsを使用する必要があった

さらにいくつかのオプションを試した後、私はファイル、レンチで武装し、意図した使用中にいくつかの不要な部品を取り除こうとしないより便利なサドル形状で自転車を書き始めることにしました。



実際、タスクは非常に簡単です。

それは単なるXMLを含むzipアーカイブだからです。

少し変更を加えて、すべてを元に戻し、他のすべてをそのままにしておくだけです。



行きましょう



これにはZipFile、lxmlなどが必要です。

import os import re from datetime import datetime, date from StringIO import StringIO from zipfile import ZipFile, ZIP_DEFLATED from lxml import etree
      
      







コンストラクター。

self._get_sheet_locations()を呼び出すことで、すぐにシートの場所を見つけます。

通常のテーブルをコンパイルして、セルインデックスから行番号を検索します



 def __init__(self, zip_folder): u""" @param zip_folder:   ,      xlsx    '[Content_Types].xml'      """ self._zip_folder = zip_folder self._data = {} self._zip_stream = StringIO() self._row_finder = re.compile(r'\d+$') self._namespaces = { 'ws': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main', 'rel': 'http://schemas.openxmlformats.org/package/2006/relationships' } self._sheet_paths = self._get_sheet_locations() #   self._shared_strings = None self._shared_strings_root = None self._shared_strings_index = None
      
      







lxml etreeオブジェクトを取得する関数を作成する

同じことをしないように、xmlファイルへのパスに沿って

 def _get_xml(self, file_path): u"""  XML-     @param file_path:       """ return etree.parse(os.path.join(self._zip_folder, file_path))
      
      







xlsxの文字列は別のファイルに保存されます。

セル(xmlノード)は、辞書のエントリのインデックスを物理的に保存します。

これはアーカイバに少し似ています。



辞書を初期化します。

新しい値を追加するのに便利なxmlオブジェクトを変換します。



これはあまり正確ではありませんが、辞書に行があるかどうかを確認して再利用することはありません。

行われた変更の量は少ないため(ファイルに既にある行の数と比較して)、

辞書に別の値を追加するだけです。



必要になります

_shared_strings(それから、変更された辞書を保存します)

_shared_strings_root-新しい行を追加します

_shared_strings_index-現在のインデックス(追加された行のカウンターを追跡するため)

 def _init_shared_strings(self): u"""       .  -        .     self._add_shared_string """ self._shared_strings = self._get_xml('xl/sharedStrings.xml') self._shared_strings_root = self._shared_strings.xpath('/ws:sst', namespaces=self._namespaces)[0] self._shared_strings_index = int(self._shared_strings_root.attrib['uniqueCount'])
      
      







辞書に行を追加し、そのインデックスを返します。

これは、文字列値を変更するために必要になります。

 def _add_shared_string(self, value): u"""     sharedStrings    ,      .  -  -     . uniqueCount  Count   (    ) @param value:      @return:    sharedStrings """ if self._shared_strings is None: self._init_shared_strings() node_t = etree.Element('t') node_t.text = value node_si = etree.Element('si') node_si.append(node_t) self._shared_strings_root.append(node_si) self._shared_strings_index += 1 return (self._shared_strings_index - 1)
      
      







アーカイブ内のどのシートに関するデータを収集します。

このデータは、2つのXMLファイルに分散されています。

-xl / workbook.xml

本が保存されている情報はどこですか

-xl / _rels / workbook.xml.rels

どこにあるかについての情報はどこにありますか

 def _get_sheet_locations(self): u"""     @return: . {_: __xml} """ #  sheets_id = {} workbook_xml = self._get_xml('xl/workbook.xml') for sheet_xml in workbook_xml.xpath('/ws:workbook/ws:sheets/ws:sheet', namespaces=self._namespaces): sheet_name = sheet_xml.attrib['name'] sheet_rid = sheet_xml.attrib['{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id'] sheets_id[sheet_rid] = sheet_name #   paths = {} xml = self._get_xml('xl/_rels/workbook.xml.rels') for node in xml.xpath('/rel:Relationships/rel:Relationship', namespaces=self._namespaces): r_id = node.attrib['Id'] path = os.path.join('xl', node.attrib['Target']) if r_id in sheets_id: sheet_label = sheets_id[r_id] paths[sheet_label] = path return paths
      
      







zipアーカイブを作成し、変更されていない(exclude_filesなしで)すべてのファイルを収集します

 def _create_base_zip(self, exclude_files): u"""         zip_folder  .        .       _add_changes @param exclude_files:    @return:  ZipFile """ zip_file = ZipFile(self._zip_stream, mode='a', compression=ZIP_DEFLATED) for path, dirs, files in os.walk(self._zip_folder): rel_path = path[len(self._zip_folder):] for file_name in files: if rel_path == '': zip_name = file_name else: zip_name = os.path.join(rel_path, file_name) if zip_name not in exclude_files: zip_file.write(os.path.join(path, file_name), zip_name) return zip_file
      
      







引数として渡されたzipファイルに変更されたシートを追加します。

シートに蓄積されたすべてのデータを確認し、変更されたシートをリクエストして、アーカイブに追加します

 def _add_changes(self, zip_file): u"""  .         zip     @param zip_file:  ZipFile  ,      """ #     for sheet_name, data in self._data.items(): sheet_file = self._sheet_paths[sheet_name] sheet_content = self._get_changed_sheet(sheet_file=sheet_file, data=data) zip_file.writestr(sheet_file, sheet_content)
      
      







シートに変更を加え、変更を含むXML文字列を返します

 def _get_changed_sheet(self, sheet_file, data): u"""         ZIP- @param sheet_file:   xml-   @param data:    {cell: value} @return: xml-    """ xml = etree.parse(os.path.join(self._zip_folder, sheet_file)) for cell, value in data.items(): self._change_cell(xml, cell, value) return etree.tostring(xml, xml_declaration=True, encoding="UTF-8", standalone="yes")
      
      







シート、セルのアドレス、値を入力として、別のセルを編集します。

これは非常に重要なポイントです。

XMLのセルは次のようになります(セルの値の変更を検討しているため)。

このセルは通常、フォーマットされ、色付けされています。

このソリューションでは、セルを持つノードが存在しないことを考慮していません。

セルを連続して表示する処理を自分でいつでも追加できます

または本の行自体。

 def _change_cell(self, xml, cell, value): u"""      xml       xml @param xml:  lxml @param cell:     "C2" @param value:  """ row_index = self._row_finder.search(cell).group() value_type = type(value) pattern_params = {'row_index': row_index, 'cell': cell} pattern = '/ws:worksheet/ws:sheetData/ws:row[@r="%(row_index)s"]/ws:c[@r="%(cell)s"]' % pattern_params node_c = xml.xpath(pattern, namespaces=self._namespaces)[0] node_v = node_c.find('ws:v', namespaces=self._namespaces) #     -    if node_v is None: node_v = etree.Element('v') node_c.append(node_v) #  if value == None: node_c.remove(node_v) if node_c.attrib.get('t') == 's': del node_c.attrib['t'] #   elif value_type in (unicode, str): value = str(self._add_shared_string(value)) node_c.attrib['t'] = 's' #       else: if node_c.attrib.get('t') == 's': del node_c.attrib['t'] if value_type == datetime: value = value.date() if value_type == date: value = (value - date(1899, 12, 30)).days node_v.text = unicode(value)
      
      







パブリックメソッドは2つしかありません。



辞書の変更を収集します。 この段階では、変更は行いません。

ここでは、単純なデータ型のみを渡すことができます。

-なし

-数値(int、float)

-文字列(str、unicode)

-日付

 def write(self, sheet, cell, value): u"""           .       - xlsx @param sheet:   @param cell:   ( C4) @param value:      """ if value is not None and type(value) not in (int, float, str, unicode): raise TypeError(u' None, int, float, str, unicode') if sheet not in self._data: self._data[sheet] = {} self._data[sheet][cell] = value
      
      







アーカイブコンテンツを取得します。

コンテンツがより普遍的であるため、ファイル、zipアーカイブとしては行いませんでした

HttpResponseを介して提供すると便利です

 def get_content(self): u"""    xlsx  . ,     ,   """ exclude_files = ['/%s' % e[1] for e in self._sheet_paths.items() if e[0] in self._data.keys()] exclude_files.append('/xl/sharedStrings.xml') zip_file = self._create_base_zip(exclude_files=exclude_files) self._add_changes(zip_file) zip_file.writestr('xl/sharedStrings.xml', etree.tostring(self._shared_strings, xml_declaration=True, encoding="UTF-8", standalone="yes")) zip_file.close() return self._zip_stream.getvalue()
      
      







のように、それがすべてです。

次のように使用します。

 xlsx = XLSXEdit('path_to_unzip_folder') xlsx.write('Sheet1', 'A1', 333) xlsx.write('Sheet1', 'A2', 44444) xlsx.write('Sheet1', 'A3', datetime.now()) xlsx.write('Sheet1', 'A4', u'') with open('/Users/dibrovsd/Desktop/out.xlsx', 'w') as zip_file: zip_file.write(xlsx.get_content())
      
      







このソリューションの最大の利点は、何が起こっているのかがはっきりしていることだと思います

複雑なことは何もありません。 必要に応じて、ソリューションを改良できます。

現在の形式ではあなたに合わない場合。



最後に、これは何ですか
 import os import re from datetime import datetime, date from StringIO import StringIO from zipfile import ZipFile, ZIP_DEFLATED from lxml import etree class XLSXEdit(object): u"""   xlsx     xml :   ,        , ,  -,   , MS query  (-  excel)         :      XML       """ def __init__(self, zip_folder): u""" @param zip_folder:   ,      xlsx    '[Content_Types].xml'      """ self._zip_folder = zip_folder self._data = {} self._zip_stream = StringIO() self._row_finder = re.compile(r'\d+$') self._namespaces = { 'ws': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main', 'rel': 'http://schemas.openxmlformats.org/package/2006/relationships' } self._sheet_paths = self._get_sheet_locations() #   self._shared_strings = None self._shared_strings_root = None self._shared_strings_index = None def write(self, sheet, cell, value): u"""           .       - xlsx @param sheet:   @param cell:   ( C4) @param value:      """ if value is not None and type(value) not in (int, float, str, unicode, date, datetime): raise TypeError(u' None, int, float, str, unicode') if sheet not in self._data: self._data[sheet] = {} self._data[sheet][cell] = value def get_content(self): u"""    xlsx  . ,     ,   """ exclude_files = ['/%s' % e[1] for e in self._sheet_paths.items() if e[0] in self._data.keys()] exclude_files.append('/xl/sharedStrings.xml') zip_file = self._create_base_zip(exclude_files=exclude_files) self._add_changes(zip_file) zip_file.writestr('xl/sharedStrings.xml', etree.tostring(self._shared_strings, xml_declaration=True, encoding="UTF-8", standalone="yes")) zip_file.close() return self._zip_stream.getvalue() def _get_xml(self, file_path): u"""  XML-     @param file_path:       """ return etree.parse(os.path.join(self._zip_folder, file_path)) def _init_shared_strings(self): u"""       .  -        .     self._add_shared_string """ self._shared_strings = self._get_xml('xl/sharedStrings.xml') self._shared_strings_root = self._shared_strings.xpath('/ws:sst', namespaces=self._namespaces)[0] self._shared_strings_index = int(self._shared_strings_root.attrib['uniqueCount']) def _add_shared_string(self, value): u"""     sharedStrings    ,      .  -  -     . uniqueCount  Count   (    ) @param value:      @return:    sharedStrings """ if self._shared_strings is None: self._init_shared_strings() node_t = etree.Element('t') node_t.text = value node_si = etree.Element('si') node_si.append(node_t) self._shared_strings_root.append(node_si) self._shared_strings_index += 1 return (self._shared_strings_index - 1) def _get_sheet_locations(self): u"""     @return: . {_: __xml} """ #  sheets_id = {} workbook_xml = self._get_xml('xl/workbook.xml') for sheet_xml in workbook_xml.xpath('/ws:workbook/ws:sheets/ws:sheet', namespaces=self._namespaces): sheet_name = sheet_xml.attrib['name'] sheet_rid = sheet_xml.attrib['{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id'] sheets_id[sheet_rid] = sheet_name #   paths = {} xml = self._get_xml('xl/_rels/workbook.xml.rels') for node in xml.xpath('/rel:Relationships/rel:Relationship', namespaces=self._namespaces): r_id = node.attrib['Id'] path = os.path.join('xl', node.attrib['Target']) if r_id in sheets_id: sheet_label = sheets_id[r_id] paths[sheet_label] = path return paths def _create_base_zip(self, exclude_files): u"""         zip_folder  .        .       _add_changes @param exclude_files:    @return:  ZipFile """ zip_file = ZipFile(self._zip_stream, mode='a', compression=ZIP_DEFLATED) for path, dirs, files in os.walk(self._zip_folder): rel_path = path[len(self._zip_folder):] for file_name in files: if rel_path == '': zip_name = file_name else: zip_name = os.path.join(rel_path, file_name) if zip_name not in exclude_files: zip_file.write(os.path.join(path, file_name), zip_name) return zip_file def _add_changes(self, zip_file): u"""  .         zip     @param zip_file:  ZipFile  ,      """ #     for sheet_name, data in self._data.items(): sheet_file = self._sheet_paths[sheet_name] sheet_content = self._get_changed_sheet(sheet_file=sheet_file, data=data) zip_file.writestr(sheet_file, sheet_content) def _get_changed_sheet(self, sheet_file, data): u"""         ZIP- @param sheet_file:   xml-   @param data:    {cell: value} @return: xml-    """ xml = etree.parse(os.path.join(self._zip_folder, sheet_file)) for cell, value in data.items(): self._change_cell(xml, cell, value) return etree.tostring(xml, xml_declaration=True, encoding="UTF-8", standalone="yes") def _change_cell(self, xml, cell, value): u"""      xml       xml @param xml:  lxml @param cell:     "C2" @param value:  """ row_index = self._row_finder.search(cell).group() value_type = type(value) pattern_params = {'row_index': row_index, 'cell': cell} pattern = '/ws:worksheet/ws:sheetData/ws:row[@r="%(row_index)s"]/ws:c[@r="%(cell)s"]' % pattern_params node_c = xml.xpath(pattern, namespaces=self._namespaces)[0] node_v = node_c.find('ws:v', namespaces=self._namespaces) #     -    if node_v is None: node_v = etree.Element('v') node_c.append(node_v) #  if value == None: node_c.remove(node_v) if node_c.attrib.get('t') == 's': del node_c.attrib['t'] #   elif value_type in (unicode, str): value = str(self._add_shared_string(value)) node_c.attrib['t'] = 's' #       else: if node_c.attrib.get('t') == 's': del node_c.attrib['t'] if value_type == datetime: value = value.date() if value_type == date: value = (value - date(1899, 12, 30)).days node_v.text = unicode(value)
      
      









まだ最後まで読んでくれてありがとう。



All Articles