Refresh Excel Chart External Data Link with Python - python

I am trying to update the external data link for a chart in Excel using python. The chart sits in workbook1.xlsm and the data it references to update itself sits in external_workbook.xlsx. The reason for the separation is the data has to be updated in workbook1.xlsm periodically using python, which erases the chart if it's in workbook1.xlsm.
I've looked at various solutions but none are working for me so far. The two solutions I've tried so far include (1) refreshing the workbook programmatically and (2) running a macro in the workbook to refresh it programmatically.
Code for (1):
import win32com.client as w3c
xlapp = w3c.gencache.EnsureDispatch('Excel.Application')
xlapp.Visible = 0
xlwb = xlapp.Workbooks.Open(r'{}\{}'.format(path, fname), False, True, None)
xlwb.RefreshAll() # Runs with no errors, but doesn't refresh
time.sleep(5)
xlwb.Save()
xlapp.Quit()
Code for (2):
# ***************** #
# Excel macro - I've verified the macro works when I have the worksheet open.
Sub Update_Links()
ActiveWorkbook.UpdateLink Name:=ActiveWorkbook.LinkSources
End Sub
# ***************** #
import win32com.client as w3c
xlapp = w3c.gencache.EnsureDispatch('Excel.Application')
xlapp.Visible = 0
xlwb = xlapp.Workbooks.Open(r'{}\{}'.format(path, fname), False, True, None)
xlwb.Application.Run("{}!Module1.Update_Links".format(fname)) # Runs with no errors, but doesn't refresh
xlwb.Save()
xlapp.Quit()
The series for my chart in Excel is
# External data link for Excel chart #
=SERIES(,'...path_to_external_file...[external_workbook.xlsx]Sheet1'!$A$2:$A$2000,
'...path_to_external_file...[external_workbook.xlsx]Sheet1'!$F$2:$F$2000,1)
Could anyone provide me with an alternative solution of how to make this work?
EDIT
So I tried something simpler to test this. I created a new sheet called temp in workbook1.xlsm and tried to write a random value to cell A1 using the code below. The temp sheet is still blank after running the code.
import win32com.client as w3c
import random
xlapp = w3c.gencache.EnsureDispatch('Excel.Application')
xlapp.Visible = 0
xlwb = xlapp.Workbooks.Open(r'{}\{}'.format(path, fname), False, True, None)
books = w3c.Dispatch(xlwb)
sheet_temp = books.Sheets('temp')
sheet_temp.Cells(1,1).Value = random.random()
xlwb.RefreshAll() # Runs with no errors, but doesn't refresh
time.sleep(5)
xlwb.Save()
xlapp.Quit()
I get no errors with the code and am following examples other people have posted online. Could someone point me to where I'm going wrong with this?

The answer is I needed to open the workbook the external_workbook.xlsx prior to updating the workbook1.xlsm, so the data could be refreshed.
The working code is as follows:
import win32com.client as w3c
import random
xlapp = w3c.gencache.EnsureDispatch('Excel.Application')
xlapp.Visible = 0
# ********************************* #
# New line that fixes it #
xlwb_data = xlapp.Workbooks.Open(r'{}\{}'.format(path, 'external_workbook.xlsx'), False, True, None)
# ********************************* #
xlwb = xlapp.Workbooks.Open(r'{}\{}'.format(path, 'workbook1.xlsm'), False, True, None)
books = w3c.Dispatch(xlwb)
sheet_temp = books.Sheets('temp')
sheet_temp.Cells(1,1).Value = random.random()
xlwb.RefreshAll() # Runs with no errors, but doesn't refresh
time.sleep(5)
xlwb.Save()
xlapp.Quit()

Related

Update Links of Powerpoint using win32com (Python)

I have a PowerPoint slide which has a linked image (table) and the data for that image is in excel.
I am trying to open the PowerPoint but even after PPTApp.DisplayAlerts = False my script gets stuck due to Security pop up which asks me to update the external links.
Here is my code:
import win32com.client
PPTApp = win32com.client.Dispatch("PowerPoint.Application")
if PPTApp.Visible == False:
PPTApp.Visible = True
PPTApp.DisplayAlerts = False
#PPTApp.AskToUpdateLinks = False (this does not work)
PPTPresentation = PPTApp.Presentations.Open(r"C:\Daily_Data_Slide.pptx")
PPTPresentation.UpdateLinks = True
Just call UpdateLinks() instead of setting the attribute UpdateLinks to True, and remove the line where you are invoking alerts PPTApp.DisplayAlerts = False :
import win32com.client
PPTApp = win32com.client.Dispatch("PowerPoint.Application")
if not PPTApp.Visible:
PPTApp.Visible = True
PPTPresentation = PPTApp.Presentations.Open(r"C:\Daily_Data_Slide.pptx")
PPTPresentation.UpdateLinks()
This is what I was able to work so far related to your question on my own projects.
I will first disable the Automatic Update for liks (File->Info->Edit Liks to file) to avoid getting stuck at the Security pop up which asks to update the external links. We will be update by looping through the slides and refreshing each shape if applicable.
Code:
import win32com.client as win32
from datetime import datetime
import time
path="\\\\Servername\\share$\\folder\\"
pptfile = "filename.pptx"
pptfilename=pptfile[:pptfile.find('.')] #filename without extension
date= datetime.now().strftime('%#d-%b-%Y')
pptApp = win32.Dispatch("PowerPoint.Application")
pptApp.DisplayAlerts = 0
Presentation = pptApp.Presentations.Open(path+pptfile,0,0,0) # This hide the power point
# Then Search in all slides and if it is a Linked object (Type 10) then Refresh
for slide in Presentation.Slides:
for shape in slide.Shapes:
if shape.Type == 10:
shape.LinkFormat.Update()
Presentation.SaveAs(FileName=path+pptfilename+"("+date+").pptx") # Save a copy with the date
Presentation.Close()
pptApp.Quit()

Reading Excel File with openpyxl while populating form using Selenium takes too long

I'm filling out a web form which has input fields, dropdown menus, autocomplete fields and action buttons.
I'm pulling the data from an excel sheet using openpyxl. Initially it used to take between 3-4 seconds to populate these fields. After adding read_only=True to my readData function, it improved a bit but not as expected.
Does anyone has any suggestions on how I would be able to reduce the time it takes to populate each field? Any help is really appreciated. I'm leaving both the readData function as well as the populate_form which I use to fill out a text field as an example.
Cheers.
Method to read each cell:
workbook = openpyxl.load_workbook(file, read_only=True)
def readData(file, sheetName, row_num, column_num):
sheet = workbook.get_sheet_by_name(sheetName)
return sheet.cell(row=row_num, column=column_num).value
Method to populate input field:
def fill_out_form(driver, path, input_sel, row_num, column_num):
try:
wait_for_element(driver, "//input[#id='" + input_sel + "']", 5)
xls = readData(path, "Callcenter", row_num, column_num)
input_el = driver.find_element_by_xpath("//input[#id='" + input_sel + "']")
input_el.click()
if column_num == 9 or column_num == 40 or column_num == 67 or column_num == 121:
xls = datetime.strftime(xls,'%d/%m/%Y')
input_el.send_keys(xls)
input_el.send_keys(Keys.TAB)
loading_el = WebDriverWait(driver, 4).until(EC.presence_of_element_located((By.XPATH, "//*[#class='sk-attr js-sk-attr sk-attr--labeled sk-attr--mandatory sk-attr--infonnized sk-attr--error sk-textbox clearfix']")))
WebDriverWait(driver, 4).until(wait_not_spinning(loading_el))
except TimeoutException:
print("Loading took too much time!-Try again")
Unless your spreadsheet is huge I'm fairly certain the wait_for_element and WebDriverWait calls are taking the most time.
As was already suggested, try caching the spreadsheet(s) data using an efficient structure such as:
dict[file][sheet] = list[row][column]
Since it seems you only have one file you can load the data using:
def load_data(filename):
data = {}
workbook = openpyxl.load_workbook(filename, data_only=True, read_only=True, keep_vba=False)
for sheet_name in workbook.sheetnames:
data[sheet_name] = []
sheet = workbook[sheet_name]
for rows in sheet.iter_rows():
row_elements = []
for cell in rows:
try:
value = cell.value
except IndexError:
value = cell.internal_value
row_elements.append(value)
data[sheet_name].append(row_elements)
return data
In order to use it, you would call load_data(filename) once (when your application starts) and access the loaded data later on using xls_data instead of readData:
#application start
xls_data = load_data(filename)
....
# sheet_name->str, row_num->int, col_num->int
xls = xls_data[sheet_name][row_num][col_num]
The above will throw KeyError if the sheet name is invalid or IndexError for an invalid row,column combination.
Try implementing the readData method using the 'xlrd' library.
It does not provide rich API like openpyxl but I'm sure it'll run faster.
When you fill in a web form, in the end the data will be sent to a server with a POST request.
What I would recommend is to use e.g. wireshark to capture that POST request.
Analyse that request to see what exactly is sent to the server. Then you can create such a POST request using the requests module.
That means you don't have to deal with selenium at all.
And as the others have mentioned, read the excel file only once.

win32com Excel PasteSpecial

I'm having some trouble with PasteSpecial in python. Here's the sample code:
import win32com.client as win32com
from win32com.client import constants
xl = win32com.gencache.EnsureDispatch('Excel.Application')
xl.Visible = True
wb = xl.Workbooks.Add ()
Sheet1 = wb.Sheets("Sheet1")
# Fill in some summy formulas
for i in range(10):
Sheet1.Cells(i+1,1).Value = "=10*"+str(i+1)
Sheet1.Range("A1:A16").Copy()
Sheet1.Range("C1").Select()
Sheet1.PasteSpecial(Paste=constants.xlPasteValues)
I'm getting the following error:
TypeError: Paste() got an unexpected keyword argument 'Paste'
I know that paste is a keyword argument because of the MSDN here:
http://msdn.microsoft.com/en-us/library/office/ff839476(v=office.15).aspx
Any idea why it won't let me do this? Can't really find much on the web.
Edit for solution(s):
import win32com.client as win32com
from win32com.client import constants
xl = win32com.gencache.EnsureDispatch('Excel.Application')
xl.Visible = True
wb = xl.Workbooks.Add ()
Sheet1 = wb.Sheets("Sheet1")
# Fill in some summy formulas
for i in range(10):
Sheet1.Cells(i+1,1).Value = "=10*"+str(i+1)
Sheet1.Range("A1:A16").Copy()
Sheet1.Range("C1").PasteSpecial(Paste=constants.xlPasteValues)
# OR this I just found right after I posted this works as well:
xl.Selection.PasteSpecial(Paste=constants.xlPasteValues)
You can get value for xlPasteFormats by execute macro in Excel vb:
Sub Macro2()
Range("A7").Select
ActiveCell.FormulaR1C1 = xlPasteFormats
End Sub
The value for xlPasteFormats is -4122
In Python script you can use
xlSheet.Range("A7:H7").Copy()
xlSheet.Range("A%s:H%s"%(r,r)).PasteSpecial(Paste=-4122)
I don't work with python but to do a PasteSpecial in Excel-VBA, you have to mention the cell where you want to perform the pastespecial, so try like
Sheet1.Range("C1").PasteSpecial(Paste=constants.xlPasteValues)
If you want a simple paste then I guess this should work
Sheet1.Paste

AutoFilter method of Range class failed (Dispatch vs EnsureDispatch)

This code fails with error: "AutoFilter method of Range class failed"
from win32com.client.gencache import EnsureDispatch
excel = EnsureDispatch('Excel.Application')
excel.Visible = 1
workbook = excel.Workbooks.Add()
sheet = workbook.ActiveSheet
sheet.Cells(1, 1).Value = 'Hello world'
sheet.Columns.AutoFilter()
This code also fails although it used to work:
from win32com.client import Dispatch
excel = Dispatch('Excel.Application')
excel.Visible = 1
workbook = excel.Workbooks.Add()
sheet = excel.ActiveSheet
sheet.Cells(1, 1).Value = 'Hello world'
sheet.Columns.AutoFilter()
Python uses win32com to communicate directly with Windows applications, and can work with (via EnsureDispatch) or without (via Dispatch) prior knowledge of the application's API. When you call EnsureDispatch, the API is fetched and written into win32com.gen_py., thereby permanently adding the application's API into your Python library.
Once you've initialised an application with EnsureDispatch, any time that a script uses Dispatch for that application, it will be given the pre-fetched API. This is good, because you can then make use of the predefined application constants (from win32com.client import constants).
However, sometimes previously working code will break. For example, in the following code, AutoFilter() will work without an argument as long as the Excel API has never previously been cached in the library...
# ExcelAutoFilterTest1
# Works unless you ever previously called EnsureDispatch('Excel.Application')
from win32com.client import Dispatch
excel = Dispatch('Excel.Application')
excel.Visible = 1
workbook = excel.Workbooks.Add()
sheet = workbook.ActiveSheet
sheet.Cells(1, 1).Value = 'Hello world'
sheet.Columns.AutoFilter()
The following code will always fail because now the Excel API has been fetched and written to win32com.gen_py.00020813-0000-0000-C000-000000000046x0x1x7 in your Python library, it will no longer accept AutoFilter() without an argument.
# ExcelAutoFilterTest2
# Always fails with error: AutoFilter method of Range class failed
from win32com.client.gencache import EnsureDispatch
excel = EnsureDispatch('Excel.Application')
excel.Visible = 1
workbook = excel.Workbooks.Add()
sheet = workbook.ActiveSheet
sheet.Cells(1, 1).Value = 'Hello world'
sheet.Columns.AutoFilter()
The following code always works because we're now providing the VisibleDropDown argument (1=on, 0=off).
# ExcelAutoFilterTest3
# Always succeeds
from win32com.client.gencache import EnsureDispatch
excel = EnsureDispatch('Excel.Application')
excel.Visible = 1
workbook = excel.Workbooks.Add()
sheet = workbook.ActiveSheet
sheet.Cells(1, 1).Value = 'Hello world'
sheet.Columns.AutoFilter(1)
This seems to be a bug, because the Excel API documentation claims that all arguments to AutoFilter are optional:
"If you omit all the arguments, this method simply toggles the display
of the AutoFilter drop-down arrows in the specified range."

Adding an Excel Textbox with win32com

I am trying to add an Excel Textbox to a worksheet... the typical shortcut I use in the Excel GUI is Alt+N X and then click where I want the Textbox; however, I don't have access to the COM browser, which leaves me guessing where Microsoft hid the Textbox API under Python's win32com...
from win32com import client
excel=client.Dispatch("Excel.Application")
excel.Visible=True
book=excel.Workbooks.Open("c:/Users/dpennington/Desktop/Blank.xls", False,
True)
sheet=book.Worksheets(2)
How would I add a textbox (i.e. in the Excel GUI: Alt+N X), using Python's win32com api? (Specific positioning in the worksheet is up to you...)
Use the AddTextbox method of the Shapes object:
import win32com.client as client
xl = client.Dispatch("Excel.Application")
xl.Visible = True
wb = xl.Workbooks.Open("c:/1temp/badacres.xls")
ws = wb.Sheets(1)
tb = ws.Shapes.AddTextbox(1, 570, 45, 171, 80)
tb.TextFrame2.TextRange.Characters.Text = 'This is a great big test.'
You can find more on the AddTextbox method here.

Categories

Resources