_reader.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. # Copyright (c) 2010-2024 openpyxl
  2. """Reader for a single worksheet."""
  3. from copy import copy
  4. from warnings import warn
  5. # compatibility imports
  6. from openpyxl.xml.functions import iterparse
  7. # package imports
  8. from openpyxl.cell import Cell, MergedCell
  9. from openpyxl.cell.text import Text
  10. from openpyxl.worksheet.dimensions import (
  11. ColumnDimension,
  12. RowDimension,
  13. SheetFormatProperties,
  14. )
  15. from openpyxl.xml.constants import (
  16. SHEET_MAIN_NS,
  17. EXT_TYPES,
  18. )
  19. from openpyxl.formatting.formatting import ConditionalFormatting
  20. from openpyxl.formula.translate import Translator
  21. from openpyxl.utils import (
  22. get_column_letter,
  23. coordinate_to_tuple,
  24. )
  25. from openpyxl.utils.datetime import from_excel, from_ISO8601, WINDOWS_EPOCH
  26. from openpyxl.descriptors.excel import ExtensionList
  27. from openpyxl.cell.rich_text import CellRichText
  28. from .formula import DataTableFormula, ArrayFormula
  29. from .filters import AutoFilter
  30. from .header_footer import HeaderFooter
  31. from .hyperlink import HyperlinkList
  32. from .merge import MergeCells
  33. from .page import PageMargins, PrintOptions, PrintPageSetup
  34. from .pagebreak import RowBreak, ColBreak
  35. from .protection import SheetProtection
  36. from .scenario import ScenarioList
  37. from .views import SheetViewList
  38. from .datavalidation import DataValidationList
  39. from .table import TablePartList
  40. from .properties import WorksheetProperties
  41. from .dimensions import SheetDimension
  42. from .related import Related
  43. CELL_TAG = '{%s}c' % SHEET_MAIN_NS
  44. VALUE_TAG = '{%s}v' % SHEET_MAIN_NS
  45. FORMULA_TAG = '{%s}f' % SHEET_MAIN_NS
  46. MERGE_TAG = '{%s}mergeCells' % SHEET_MAIN_NS
  47. INLINE_STRING = "{%s}is" % SHEET_MAIN_NS
  48. COL_TAG = '{%s}col' % SHEET_MAIN_NS
  49. ROW_TAG = '{%s}row' % SHEET_MAIN_NS
  50. CF_TAG = '{%s}conditionalFormatting' % SHEET_MAIN_NS
  51. LEGACY_TAG = '{%s}legacyDrawing' % SHEET_MAIN_NS
  52. PROT_TAG = '{%s}sheetProtection' % SHEET_MAIN_NS
  53. EXT_TAG = "{%s}extLst" % SHEET_MAIN_NS
  54. HYPERLINK_TAG = "{%s}hyperlinks" % SHEET_MAIN_NS
  55. TABLE_TAG = "{%s}tableParts" % SHEET_MAIN_NS
  56. PRINT_TAG = '{%s}printOptions' % SHEET_MAIN_NS
  57. MARGINS_TAG = '{%s}pageMargins' % SHEET_MAIN_NS
  58. PAGE_TAG = '{%s}pageSetup' % SHEET_MAIN_NS
  59. HEADER_TAG = '{%s}headerFooter' % SHEET_MAIN_NS
  60. FILTER_TAG = '{%s}autoFilter' % SHEET_MAIN_NS
  61. VALIDATION_TAG = '{%s}dataValidations' % SHEET_MAIN_NS
  62. PROPERTIES_TAG = '{%s}sheetPr' % SHEET_MAIN_NS
  63. VIEWS_TAG = '{%s}sheetViews' % SHEET_MAIN_NS
  64. FORMAT_TAG = '{%s}sheetFormatPr' % SHEET_MAIN_NS
  65. ROW_BREAK_TAG = '{%s}rowBreaks' % SHEET_MAIN_NS
  66. COL_BREAK_TAG = '{%s}colBreaks' % SHEET_MAIN_NS
  67. SCENARIOS_TAG = '{%s}scenarios' % SHEET_MAIN_NS
  68. DATA_TAG = '{%s}sheetData' % SHEET_MAIN_NS
  69. DIMENSION_TAG = '{%s}dimension' % SHEET_MAIN_NS
  70. CUSTOM_VIEWS_TAG = '{%s}customSheetViews' % SHEET_MAIN_NS
  71. def _cast_number(value):
  72. "Convert numbers as string to an int or float"
  73. if "." in value or "E" in value or "e" in value:
  74. return float(value)
  75. return int(value)
  76. def parse_richtext_string(element):
  77. """
  78. Parse inline string and preserve rich text formatting
  79. """
  80. value = CellRichText.from_tree(element) or ""
  81. if len(value) == 1 and isinstance(value[0], str):
  82. value = value[0]
  83. return value
  84. class WorkSheetParser:
  85. def __init__(self, src, shared_strings, data_only=False,
  86. epoch=WINDOWS_EPOCH, date_formats=set(),
  87. timedelta_formats=set(), rich_text=False):
  88. self.min_row = self.min_col = None
  89. self.epoch = epoch
  90. self.source = src
  91. self.shared_strings = shared_strings
  92. self.data_only = data_only
  93. self.shared_formulae = {}
  94. self.row_counter = self.col_counter = 0
  95. self.tables = TablePartList()
  96. self.date_formats = date_formats
  97. self.timedelta_formats = timedelta_formats
  98. self.row_dimensions = {}
  99. self.column_dimensions = {}
  100. self.number_formats = []
  101. self.keep_vba = False
  102. self.hyperlinks = HyperlinkList()
  103. self.formatting = []
  104. self.legacy_drawing = None
  105. self.merged_cells = None
  106. self.row_breaks = RowBreak()
  107. self.col_breaks = ColBreak()
  108. self.rich_text = rich_text
  109. def parse(self):
  110. dispatcher = {
  111. COL_TAG: self.parse_column_dimensions,
  112. PROT_TAG: self.parse_sheet_protection,
  113. EXT_TAG: self.parse_extensions,
  114. CF_TAG: self.parse_formatting,
  115. LEGACY_TAG: self.parse_legacy,
  116. ROW_BREAK_TAG: self.parse_row_breaks,
  117. COL_BREAK_TAG: self.parse_col_breaks,
  118. CUSTOM_VIEWS_TAG: self.parse_custom_views,
  119. }
  120. properties = {
  121. PRINT_TAG: ('print_options', PrintOptions),
  122. MARGINS_TAG: ('page_margins', PageMargins),
  123. PAGE_TAG: ('page_setup', PrintPageSetup),
  124. HEADER_TAG: ('HeaderFooter', HeaderFooter),
  125. FILTER_TAG: ('auto_filter', AutoFilter),
  126. VALIDATION_TAG: ('data_validations', DataValidationList),
  127. PROPERTIES_TAG: ('sheet_properties', WorksheetProperties),
  128. VIEWS_TAG: ('views', SheetViewList),
  129. FORMAT_TAG: ('sheet_format', SheetFormatProperties),
  130. SCENARIOS_TAG: ('scenarios', ScenarioList),
  131. TABLE_TAG: ('tables', TablePartList),
  132. HYPERLINK_TAG: ('hyperlinks', HyperlinkList),
  133. MERGE_TAG: ('merged_cells', MergeCells),
  134. }
  135. it = iterparse(self.source) # add a finaliser to close the source when this becomes possible
  136. for _, element in it:
  137. tag_name = element.tag
  138. if tag_name in dispatcher:
  139. dispatcher[tag_name](element)
  140. element.clear()
  141. elif tag_name in properties:
  142. prop = properties[tag_name]
  143. obj = prop[1].from_tree(element)
  144. setattr(self, prop[0], obj)
  145. element.clear()
  146. elif tag_name == ROW_TAG:
  147. row = self.parse_row(element)
  148. element.clear()
  149. yield row
  150. def parse_dimensions(self):
  151. """
  152. Get worksheet dimensions if they are provided.
  153. """
  154. it = iterparse(self.source)
  155. for _event, element in it:
  156. if element.tag == DIMENSION_TAG:
  157. dim = SheetDimension.from_tree(element)
  158. return dim.boundaries
  159. elif element.tag == DATA_TAG:
  160. # Dimensions missing
  161. break
  162. element.clear()
  163. def parse_cell(self, element):
  164. data_type = element.get('t', 'n')
  165. coordinate = element.get('r')
  166. style_id = element.get('s', 0)
  167. if style_id:
  168. style_id = int(style_id)
  169. if data_type == "inlineStr":
  170. value = None
  171. else:
  172. value = element.findtext(VALUE_TAG, None) or None
  173. if coordinate:
  174. row, column = coordinate_to_tuple(coordinate)
  175. self.col_counter = column
  176. else:
  177. self.col_counter += 1
  178. row, column = self.row_counter, self.col_counter
  179. if not self.data_only and element.find(FORMULA_TAG) is not None:
  180. data_type = 'f'
  181. value = self.parse_formula(element)
  182. elif value is not None:
  183. if data_type == 'n':
  184. value = _cast_number(value)
  185. if style_id in self.date_formats:
  186. data_type = 'd'
  187. try:
  188. value = from_excel(
  189. value, self.epoch, timedelta=style_id in self.timedelta_formats
  190. )
  191. except (OverflowError, ValueError):
  192. msg = f"""Cell {coordinate} is marked as a date but the serial value {value} is outside the limits for dates. The cell will be treated as an error."""
  193. warn(msg)
  194. data_type = "e"
  195. value = "#VALUE!"
  196. elif data_type == 's':
  197. value = self.shared_strings[int(value)]
  198. elif data_type == 'b':
  199. value = bool(int(value))
  200. elif data_type == "str":
  201. data_type = "s"
  202. elif data_type == 'd':
  203. value = from_ISO8601(value)
  204. elif data_type == 'inlineStr':
  205. child = element.find(INLINE_STRING)
  206. if child is not None:
  207. data_type = 's'
  208. if self.rich_text:
  209. value = parse_richtext_string(child)
  210. else:
  211. value = Text.from_tree(child).content
  212. return {'row':row, 'column':column, 'value':value, 'data_type':data_type, 'style_id':style_id}
  213. def parse_formula(self, element):
  214. """
  215. possible formulae types: shared, array, datatable
  216. """
  217. formula = element.find(FORMULA_TAG)
  218. formula_type = formula.get('t')
  219. coordinate = element.get('r')
  220. value = "="
  221. if formula.text is not None:
  222. value += formula.text
  223. if formula_type == "array":
  224. value = ArrayFormula(ref=formula.get('ref'), text=value)
  225. elif formula_type == "shared":
  226. idx = formula.get('si')
  227. if idx in self.shared_formulae:
  228. trans = self.shared_formulae[idx]
  229. value = trans.translate_formula(coordinate)
  230. elif value != "=":
  231. self.shared_formulae[idx] = Translator(value, coordinate)
  232. elif formula_type == "dataTable":
  233. value = DataTableFormula(**formula.attrib)
  234. return value
  235. def parse_column_dimensions(self, col):
  236. attrs = dict(col.attrib)
  237. column = get_column_letter(int(attrs['min']))
  238. attrs['index'] = column
  239. self.column_dimensions[column] = attrs
  240. def parse_row(self, row):
  241. attrs = dict(row.attrib)
  242. if "r" in attrs:
  243. try:
  244. self.row_counter = int(attrs['r'])
  245. except ValueError:
  246. val = float(attrs['r'])
  247. if val.is_integer():
  248. self.row_counter = int(val)
  249. else:
  250. raise ValueError(f"{attrs['r']} is not a valid row number")
  251. else:
  252. self.row_counter += 1
  253. self.col_counter = 0
  254. keys = {k for k in attrs if not k.startswith('{')}
  255. if keys - {'r', 'spans'}:
  256. # don't create dimension objects unless they have relevant information
  257. self.row_dimensions[str(self.row_counter)] = attrs
  258. cells = [self.parse_cell(el) for el in row]
  259. return self.row_counter, cells
  260. def parse_formatting(self, element):
  261. try:
  262. cf = ConditionalFormatting.from_tree(element)
  263. self.formatting.append(cf)
  264. except TypeError as e:
  265. msg = f"Failed to load a conditional formatting rule. It will be discarded. Cause: {e}"
  266. warn(msg)
  267. def parse_sheet_protection(self, element):
  268. protection = SheetProtection.from_tree(element)
  269. password = element.get("password")
  270. if password is not None:
  271. protection.set_password(password, True)
  272. self.protection = protection
  273. def parse_extensions(self, element):
  274. extLst = ExtensionList.from_tree(element)
  275. for e in extLst.ext:
  276. ext_type = EXT_TYPES.get(e.uri.upper(), "Unknown")
  277. msg = "{0} extension is not supported and will be removed".format(ext_type)
  278. warn(msg)
  279. def parse_legacy(self, element):
  280. obj = Related.from_tree(element)
  281. self.legacy_drawing = obj.id
  282. def parse_row_breaks(self, element):
  283. brk = RowBreak.from_tree(element)
  284. self.row_breaks = brk
  285. def parse_col_breaks(self, element):
  286. brk = ColBreak.from_tree(element)
  287. self.col_breaks = brk
  288. def parse_custom_views(self, element):
  289. # clear page_breaks to avoid duplication which Excel doesn't like
  290. # basically they're ignored in custom views
  291. self.row_breaks = RowBreak()
  292. self.col_breaks = ColBreak()
  293. class WorksheetReader:
  294. """
  295. Create a parser and apply it to a workbook
  296. """
  297. def __init__(self, ws, xml_source, shared_strings, data_only, rich_text):
  298. self.ws = ws
  299. self.parser = WorkSheetParser(xml_source, shared_strings,
  300. data_only, ws.parent.epoch, ws.parent._date_formats,
  301. ws.parent._timedelta_formats, rich_text)
  302. self.tables = []
  303. def bind_cells(self):
  304. for idx, row in self.parser.parse():
  305. for cell in row:
  306. style = self.ws.parent._cell_styles[cell['style_id']]
  307. c = Cell(self.ws, row=cell['row'], column=cell['column'], style_array=style)
  308. c._value = cell['value']
  309. c.data_type = cell['data_type']
  310. self.ws._cells[(cell['row'], cell['column'])] = c
  311. if self.ws._cells:
  312. self.ws._current_row = self.ws.max_row # use cells not row dimensions
  313. def bind_formatting(self):
  314. for cf in self.parser.formatting:
  315. for rule in cf.rules:
  316. if rule.dxfId is not None:
  317. rule.dxf = self.ws.parent._differential_styles[rule.dxfId]
  318. self.ws.conditional_formatting[cf] = rule
  319. def bind_tables(self):
  320. for t in self.parser.tables.tablePart:
  321. rel = self.ws._rels.get(t.id)
  322. self.tables.append(rel.Target)
  323. def bind_merged_cells(self):
  324. from openpyxl.worksheet.cell_range import MultiCellRange
  325. from openpyxl.worksheet.merge import MergedCellRange
  326. if not self.parser.merged_cells:
  327. return
  328. ranges = []
  329. for cr in self.parser.merged_cells.mergeCell:
  330. mcr = MergedCellRange(self.ws, cr.ref)
  331. self.ws._clean_merge_range(mcr)
  332. ranges.append(mcr)
  333. self.ws.merged_cells = MultiCellRange(ranges)
  334. def bind_hyperlinks(self):
  335. for link in self.parser.hyperlinks.hyperlink:
  336. if link.id:
  337. rel = self.ws._rels.get(link.id)
  338. link.target = rel.Target
  339. if ":" in link.ref:
  340. # range of cells
  341. for row in self.ws[link.ref]:
  342. for cell in row:
  343. try:
  344. cell.hyperlink = copy(link)
  345. except AttributeError:
  346. pass
  347. else:
  348. cell = self.ws[link.ref]
  349. if isinstance(cell, MergedCell):
  350. cell = self.normalize_merged_cell_link(cell.coordinate)
  351. cell.hyperlink = link
  352. def normalize_merged_cell_link(self, coord):
  353. """
  354. Returns the appropriate cell to which a hyperlink, which references a merged cell at the specified coordinates,
  355. should be bound.
  356. """
  357. for rng in self.ws.merged_cells:
  358. if coord in rng:
  359. return self.ws.cell(*rng.top[0])
  360. def bind_col_dimensions(self):
  361. for col, cd in self.parser.column_dimensions.items():
  362. if 'style' in cd:
  363. key = int(cd['style'])
  364. cd['style'] = self.ws.parent._cell_styles[key]
  365. self.ws.column_dimensions[col] = ColumnDimension(self.ws, **cd)
  366. def bind_row_dimensions(self):
  367. for row, rd in self.parser.row_dimensions.items():
  368. if 's' in rd:
  369. key = int(rd['s'])
  370. rd['s'] = self.ws.parent._cell_styles[key]
  371. self.ws.row_dimensions[int(row)] = RowDimension(self.ws, **rd)
  372. def bind_properties(self):
  373. for k in ('print_options', 'page_margins', 'page_setup',
  374. 'HeaderFooter', 'auto_filter', 'data_validations',
  375. 'sheet_properties', 'views', 'sheet_format',
  376. 'row_breaks', 'col_breaks', 'scenarios', 'legacy_drawing',
  377. 'protection',
  378. ):
  379. v = getattr(self.parser, k, None)
  380. if v is not None:
  381. setattr(self.ws, k, v)
  382. def bind_all(self):
  383. self.bind_cells()
  384. self.bind_merged_cells()
  385. self.bind_hyperlinks()
  386. self.bind_formatting()
  387. self.bind_col_dimensions()
  388. self.bind_row_dimensions()
  389. self.bind_tables()
  390. self.bind_properties()