web-dev-qa-db-fra.com

Insérer une ligne dans une feuille de calcul Excel en utilisant openpyxl en Python

Je cherche la meilleure approche pour insérer une ligne dans une feuille de calcul en utilisant openpyxl.

En fait, j'ai une feuille de calcul (Excel 2007) qui a une ligne d'en-tête, suivie (au plus) de quelques milliers de lignes de données. Je cherche à insérer la ligne en tant que première ligne de données réelles, donc après l'en-tête. D'après ce que j'ai compris, la fonction append convient à l'ajout de contenu à la fin du fichier.

En lisant la documentation d’openpyxl et de xlrd (et xlwt), je ne trouve aucun moyen de le faire, au-delà de la lecture manuelle du contenu et de son insertion dans une nouvelle feuille (après l’insertion de la ligne requise). 

Compte tenu de mon expérience jusque-là limitée avec Python, j'essaie de comprendre si c'est vraiment la meilleure option à prendre (le plus Pythonic!), Et si oui, quelqu'un pourrait-il donner un exemple explicite. Plus précisément, puis-je lire et écrire des lignes avec openpyxl ou dois-je accéder aux cellules? De plus, puis-je (sur) écrire le même fichier (nom)?

13
Nick

Répondre à cela avec le code que j'utilise maintenant pour obtenir le résultat souhaité. Notez que j'insère manuellement la ligne en position 1, mais cela devrait être assez facile à ajuster pour répondre à des besoins spécifiques. Vous pouvez également modifier facilement cette option pour insérer plusieurs lignes et simplement renseigner le reste des données en commençant par la position appropriée.

Notez également qu'en raison des dépendances en aval, nous spécifions manuellement les données à partir de 'Sheet1' et les données sont copiées dans une nouvelle feuille qui est insérée au début du classeur tout en renommant la feuille de calcul d'origine en 'Sheet1.5'. .

EDIT: J'ai également ajouté (plus tard) une modification au format_code pour résoudre les problèmes pour lesquels l'opération de copie par défaut supprime tout formatage: new_cell.style.number_format.format_code = 'mm/dd/yyyy'. Je ne pouvais trouver aucune documentation indiquant que cela était réglable, il s'agissait davantage d'un cas d'essai et d'erreur!

Enfin, n’oubliez pas que cet exemple permet d’économiser sur l’original. Vous pouvez modifier le chemin de sauvegarde le cas échéant pour éviter cela.

    import openpyxl

    wb = openpyxl.load_workbook(file)
    old_sheet = wb.get_sheet_by_name('Sheet1')
    old_sheet.title = 'Sheet1.5'
    max_row = old_sheet.get_highest_row()
    max_col = old_sheet.get_highest_column()
    wb.create_sheet(0, 'Sheet1')

    new_sheet = wb.get_sheet_by_name('Sheet1')

    # Do the header.
    for col_num in range(0, max_col):
        new_sheet.cell(row=0, column=col_num).value = old_sheet.cell(row=0, column=col_num).value

    # The row to be inserted. We're manually populating each cell.
    new_sheet.cell(row=1, column=0).value = 'DUMMY'
    new_sheet.cell(row=1, column=1).value = 'DUMMY'

    # Now do the rest of it. Note the row offset.
    for row_num in range(1, max_row):
        for col_num in range (0, max_col):
            new_sheet.cell(row = (row_num + 1), column = col_num).value = old_sheet.cell(row = row_num, column = col_num).value

    wb.save(file)
9
Nick

== Mise à jour vers une version entièrement fonctionnelle, basée sur les commentaires fournis ici: groups.google.com/forum/#!topic/openpyxl-users/wHGecdQg3Iw. ==

Comme d'autres l'ont souligné, openpyxl ne fournit pas cette fonctionnalité, mais j'ai étendu la classe Worksheet comme suit pour implémenter l'insertion de lignes. J'espère que cela s'avère utile pour les autres.

def insert_rows(self, row_idx, cnt, above=False, copy_style=True, fill_formulae=True):
    """Inserts new (empty) rows into worksheet at specified row index.

    :param row_idx: Row index specifying where to insert new rows.
    :param cnt: Number of rows to insert.
    :param above: Set True to insert rows above specified row index.
    :param copy_style: Set True if new rows should copy style of immediately above row.
    :param fill_formulae: Set True if new rows should take on formula from immediately above row, filled with references new to rows.

    Usage:

    * insert_rows(2, 10, above=True, copy_style=False)

    """
    CELL_RE  = re.compile("(?P<col>\$?[A-Z]+)(?P<row>\$?\d+)")

    row_idx = row_idx - 1 if above else row_idx

    def replace(m):
        row = m.group('row')
        prefix = "$" if row.find("$") != -1 else ""
        row = int(row.replace("$",""))
        row += cnt if row > row_idx else 0
        return m.group('col') + prefix + str(row)

    # First, we shift all cells down cnt rows...
    old_cells = set()
    old_fas   = set()
    new_cells = dict()
    new_fas   = dict()
    for c in self._cells.values():

        old_coor = c.coordinate

        # Shift all references to anything below row_idx
        if c.data_type == Cell.TYPE_FORMULA:
            c.value = CELL_RE.sub(
                replace,
                c.value
            )
            # Here, we need to properly update the formula references to reflect new row indices
            if old_coor in self.formula_attributes and 'ref' in self.formula_attributes[old_coor]:
                self.formula_attributes[old_coor]['ref'] = CELL_RE.sub(
                    replace,
                    self.formula_attributes[old_coor]['ref']
                )

        # Do the magic to set up our actual shift    
        if c.row > row_idx:
            old_coor = c.coordinate
            old_cells.add((c.row,c.col_idx))
            c.row += cnt
            new_cells[(c.row,c.col_idx)] = c
            if old_coor in self.formula_attributes:
                old_fas.add(old_coor)
                fa = self.formula_attributes[old_coor].copy()
                new_fas[c.coordinate] = fa

    for coor in old_cells:
        del self._cells[coor]
    self._cells.update(new_cells)

    for fa in old_fas:
        del self.formula_attributes[fa]
    self.formula_attributes.update(new_fas)

    # Next, we need to shift all the Row Dimensions below our new rows down by cnt...
    for row in range(len(self.row_dimensions)-1+cnt,row_idx+cnt,-1):
        new_rd = copy.copy(self.row_dimensions[row-cnt])
        new_rd.index = row
        self.row_dimensions[row] = new_rd
        del self.row_dimensions[row-cnt]

    # Now, create our new rows, with all the pretty cells
    row_idx += 1
    for row in range(row_idx,row_idx+cnt):
        # Create a Row Dimension for our new row
        new_rd = copy.copy(self.row_dimensions[row-1])
        new_rd.index = row
        self.row_dimensions[row] = new_rd
        for col in range(1,self.max_column):
            col = get_column_letter(col)
            cell = self.cell('%s%d'%(col,row))
            cell.value = None
            source = self.cell('%s%d'%(col,row-1))
            if copy_style:
                cell.number_format = source.number_format
                cell.font      = source.font.copy()
                cell.alignment = source.alignment.copy()
                cell.border    = source.border.copy()
                cell.fill      = source.fill.copy()
            if fill_formulae and source.data_type == Cell.TYPE_FORMULA:
                s_coor = source.coordinate
                if s_coor in self.formula_attributes and 'ref' not in self.formula_attributes[s_coor]:
                    fa = self.formula_attributes[s_coor].copy()
                    self.formula_attributes[cell.coordinate] = fa
                # print("Copying formula from cell %s%d to %s%d"%(col,row-1,col,row))
                cell.value = re.sub(
                    "(\$?[A-Z]{1,3}\$?)%d"%(row - 1),
                    lambda m: m.group(1) + str(row),
                    source.value
                )   
                cell.data_type = Cell.TYPE_FORMULA

    # Check for Merged Cell Ranges that need to be expanded to contain new cells
    for cr_idx, cr in enumerate(self.merged_cell_ranges):
        self.merged_cell_ranges[cr_idx] = CELL_RE.sub(
            replace,
            cr
        )

Worksheet.insert_rows = insert_rows
17
Dallas

Les feuilles de calcul Openpyxl ont des fonctionnalités limitées pour effectuer des opérations au niveau des lignes ou des colonnes. Les seules propriétés d'une feuille de travail concernant les lignes/colonnes sont les propriétés row_dimensions et column_dimensions, qui stockent des objets "RowDimensions" et "ColumnDimensions" pour chaque ligne et colonne, respectivement. Ces dictionnaires sont également utilisés dans des fonctions telles que get_highest_row() et get_highest_column().

Tout le reste fonctionne au niveau de la cellule, les objets Cell étant suivis dans le dictionnaire, _cells (et leur style suivi dans le dictionnaire _styles). La plupart des fonctions qui donnent l'impression de faire quoi que ce soit au niveau d'une ligne ou d'une colonne fonctionnent en fait sur une plage de cellules (telle que la append() susmentionnée).

La solution la plus simple serait celle que vous avez suggérée: créer une nouvelle feuille, ajouter votre ligne d’en-tête, ajouter de nouvelles lignes de données, ajouter vos anciennes lignes de données, supprimer l’ancienne feuille, puis renommer votre nouvelle feuille. Les problèmes pouvant être présentés avec cette méthode sont la perte d'attributs de dimensions de ligne/colonne et de styles de cellule, à moins que vous ne les copiiez également.

Vous pouvez également créer vos propres fonctions qui insèrent des lignes ou des colonnes.

J'avais un grand nombre de très feuilles de calcul simples à supprimer. Puisque vous avez demandé des exemples explicites, je vais vous fournir la fonction que j’ai rapidement élaborée:

from openpyxl.cell import get_column_letter

def ws_delete_column(sheet, del_column):

    for row_num in range(1, sheet.get_highest_row()+1):
        for col_num in range(del_column, sheet.get_highest_column()+1):

            coordinate = '%s%s' % (get_column_letter(col_num),
                                   row_num)
            adj_coordinate = '%s%s' % (get_column_letter(col_num + 1),
                                       row_num)

            # Handle Styles.
            # This is important to do if you have any differing
            # 'types' of data being stored, as you may otherwise get
            # an output Worksheet that's got improperly formatted cells.
            # Or worse, an error gets thrown because you tried to copy
            # a string value into a cell that's styled as a date.

            if adj_coordinate in sheet._styles:
                sheet._styles[coordinate] = sheet._styles[adj_coordinate]
                sheet._styles.pop(adj_coordinate, None)
            else:
                sheet._styles.pop(coordinate, None)

            if adj_coordinate in sheet._cells:
                sheet._cells[coordinate] = sheet._cells[adj_coordinate]
                sheet._cells[coordinate].column = get_column_letter(col_num)
                sheet._cells[coordinate].row = row_num
                sheet._cells[coordinate].coordinate = coordinate

                sheet._cells.pop(adj_coordinate, None)
            else:
                sheet._cells.pop(coordinate, None)

        # sheet.garbage_collect()

Je lui transmets la feuille de calcul avec laquelle je travaille et le numéro de colonne que je veux supprimer, puis il disparaît. Je sais que ce n'est pas exactement ce que vous vouliez, mais j'espère que cette information vous a aidé!

EDIT: Remarqué que quelqu'un a donné un autre vote, et a pensé que je devrais le mettre à jour. Le système de coordonnées dans Openpyxl a connu quelques changements au cours des dernières années, en introduisant un attribut coordinate pour les éléments dans _cell. Cela doit également être modifié, sinon les lignes resteront vides (au lieu d'être supprimées) et Excel générera une erreur concernant les problèmes rencontrés avec le fichier. Cela fonctionne pour Openpyxl 2.2.3 (non testé avec les versions ultérieures)

5
Rejected

Depuis openpyxl 1.5, vous pouvez maintenant utiliser .insert_rows (idx, row_qty)

from openpyxl import load_workbook
wb = load_workbook('Excel_template.xlsx')
ws = wb.active
ws.insert_rows(14, 10)

La mise en forme de la ligne idx ne sera pas prise en compte comme si vous le faisiez manuellement dans Excel. vous devrez ensuite appliquer le formatage correct, c'est-à-dire la couleur de la cellule.

1
PrestonDocks

J'ai pris la solution de Dallas et ajouté un support pour les cellules fusionnées:

    def insert_rows(self, row_idx, cnt, above=False, copy_style=True, fill_formulae=True):
        skip_list = []
        try:
            idx = row_idx - 1 if above else row_idx
            for (new, old) in Zip(range(self.max_row+cnt,idx+cnt,-1),range(self.max_row,idx,-1)):
                for c_idx in range(1,self.max_column):
                  col = self.cell(row=1, column=c_idx).column #get_column_letter(c_idx)
                  print("Copying %s%d to %s%d."%(col,old,col,new))
                  source = self["%s%d"%(col,old)]
                  target = self["%s%d"%(col,new)]
                  if source.coordinate in skip_list:
                      continue

                  if source.coordinate in self.merged_cells:
                      # This is a merged cell
                      for _range in self.merged_cell_ranges:
                          merged_cells_list = [x for x in cells_from_range(_range)][0]
                          if source.coordinate in merged_cells_list:
                              skip_list = merged_cells_list
                              self.unmerge_cells(_range)
                              new_range = re.sub(str(old),str(new),_range)
                              self.merge_cells(new_range)
                              break

                  if source.data_type == Cell.TYPE_FORMULA:
                    target.value = re.sub(
                      "(\$?[A-Z]{1,3})%d"%(old),
                      lambda m: m.group(1) + str(new),
                      source.value
                    )
                  else:
                    target.value = source.value
                  target.number_format = source.number_format
                  target.font   = source.font.copy()
                  target.alignment = source.alignment.copy()
                  target.border = source.border.copy()
                  target.fill   = source.fill.copy()
            idx = idx + 1
            for row in range(idx,idx+cnt):
                for c_idx in range(1,self.max_column):
                  col = self.cell(row=1, column=c_idx).column #get_column_letter(c_idx)
                  #print("Clearing value in cell %s%d"%(col,row))
                  cell = self["%s%d"%(col,row)]
                  cell.value = None
                  source = self["%s%d"%(col,row-1)]
                  if copy_style:
                    cell.number_format = source.number_format
                    cell.font      = source.font.copy()
                    cell.alignment = source.alignment.copy()
                    cell.border    = source.border.copy()
                    cell.fill      = source.fill.copy()
                  if fill_formulae and source.data_type == Cell.TYPE_FORMULA:
                    #print("Copying formula from cell %s%d to %s%d"%(col,row-1,col,row))
                    cell.value = re.sub(
                      "(\$?[A-Z]{1,3})%d"%(row - 1),
                      lambda m: m.group(1) + str(row),
                      source.value
                    )
1
Ran S

Pour insérer une ligne dans une feuille de calcul Excel à l'aide de openpyxl en Python

Le code ci-dessous peut vous aider: -

import openpyxl

file = "xyz.xlsx"
#loading XL sheet bassed on file name provided by user
book = openpyxl.load_workbook(file)
#opening sheet whose index no is 0
sheet = book.worksheets[0]

#insert_rows(idx, amount=1) Insert row or rows before row==idx, amount will be no of 
#rows you want to add and it's optional
sheet.insert_rows(13)

Pour insérer une colonne, openpyxl a également une fonction similaire, i.e.insert_cols (idx, amount = 1)

0
yugal sinha

Cela a fonctionné pour moi:

    openpyxl.worksheet.worksheet.Worksheet.insert_rows(wbs,idx=row,amount=2)

Insérer 2 lignes avant la ligne == idx

Voir: http://openpyxl.readthedocs.io/fr/stable/api/openpyxl.worksheet.worksheet.html

0
ack

La solution de Nick modifiée, cette version prend une ligne de départ, le nombre de lignes à insérer et un nom de fichier, et insère le nombre nécessaire de lignes vides.

#! python 3

import openpyxl, sys

my_start = int(sys.argv[1])
my_rows = int(sys.argv[2])
str_wb = str(sys.argv[3])

wb = openpyxl.load_workbook(str_wb)
old_sheet = wb.get_sheet_by_name('Sheet')
mcol = old_sheet.max_column
mrow = old_sheet.max_row
old_sheet.title = 'Sheet1.5'
wb.create_sheet(index=0, title='Sheet')

new_sheet = wb.get_sheet_by_name('Sheet')

for row_num in range(1, my_start):
    for col_num in range(1, mcol + 1):
        new_sheet.cell(row = row_num, column = col_num).value = old_sheet.cell(row = row_num, column = col_num).value

for row_num in range(my_start + my_rows, mrow + my_rows):
    for col_num in range(1, mcol + 1):
        new_sheet.cell(row = (row_num + my_rows), column = col_num).value = old_sheet.cell(row = row_num, column = col_num).value

wb.save(str_wb)
0
mut3