-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain_program.py
More file actions
175 lines (151 loc) · 6.93 KB
/
Copy pathmain_program.py
File metadata and controls
175 lines (151 loc) · 6.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
from multiprocessing import Pool, cpu_count
import regex as re
from typing import List, Optional, Tuple, Union, Callable, Any
import pandas as pd
import xlwings as xw
from xlwings.main import Book, Range
import traceback
import logging
from argparse import ArgumentParser, ArgumentTypeError
from functools import partial
import timeit
logging.basicConfig(
format='%(asctime)s %(levelname)-8s %(message)s',
level=logging.INFO,
datefmt='%Y-%m-%d %H:%M:%S')
# E.g. "[C:\Users\[username]\Desktop\Book1.xlsx]Sheet1!A1:B5"
path_pattern = re.compile(r"^\[(.+?)\](.+?)!(.+)$")
my_date_handler = lambda year, month, day, **kwargs: "%04i-%02i-%02i" % (year, month, day)
def get_sheet_names(book: Book) -> List[str]:
return [sheet.name for sheet in book.sheets]
def normalize_data(shape: Tuple[int, int], input) -> List[List[str]]:
"""
Constructing pd.DataFrame requires a List of Lists. Data must be normalized accordingly.
"""
row, column = shape
if row == 1 and column == 1:
return [[input]]
elif row == 1 and column > 1:
return [input]
elif row > 1 and column == 1:
return list(map(lambda x: [x], input))
elif row > 1 and column > 1:
return input
else:
raise ValueError(f"Shape invalid: [{shape}]")
def extract_cell_values(excel_range: Range) -> pd.DataFrame:
data = excel_range.options(dates=my_date_handler, numbers=str, empty="").value
return pd.DataFrame(normalize_data(excel_range.shape, data))
def extract_cell_formulas(excel_range: Range) -> pd.DataFrame:
data = excel_range.options(dates=str, numbers=str, empty="").formula
return pd.DataFrame(normalize_data(excel_range.shape, data))
def get_cell_value(cell) -> str:
"""
Try getting a cell value. If it is in a merged area, get the value of the merged area.
"""
if cell.value is not None:
return str(cell.value)
elif cell.MergeArea.Cells(1, 1).Value is not None:
return str(cell.MergeArea.Cells(1, 1).Value)
return ""
def extract_cell_property_by_enumerating(excel_range: Range, extract_func: Callable[[Any], str]):
"""
The slowest method, but also the most flexible (any type of properties can be extracted).
"""
data = [[extract_func(excel_range.api.Cells(row_idx, col_idx)) for col_idx in range(1, excel_range.api.Columns.Count + 1)]
for row_idx in range(1, excel_range.api.Rows.Count + 1)]
return pd.DataFrame(data)
def initialize():
global cached_excels
cached_excels = {}
def extracting_data(request: str, extract_func: Callable[[Range], pd.DataFrame]) -> Tuple[bool, Union[pd.DataFrame, str]]:
result: Tuple[bool, Union[pd.DataFrame, str]] = (False, "")
try:
if request == "EXIT":
for (_, value) in cached_excels.items():
(temp_app, temp_book) = value
temp_book.close()
temp_app.kill()
result = (True, "")
else:
matches = re.search(path_pattern, request)
if matches:
(excel_path, excel_sheet, excel_range) = (matches.group(1), matches.group(2), matches.group(3))
if excel_path not in cached_excels:
temp_app = xw.App(visible=False)
temp_app.calculation = "manual"
temp_app.screen_updating = False
try:
temp_book = temp_app.books.open(excel_path)
cached_excels[excel_path] = (temp_app, temp_book)
except:
temp_app.kill() # if file is problematic, kill the temp instance.
raise
if excel_sheet not in get_sheet_names(cached_excels[excel_path][1]):
raise ValueError(f"Unknown sheet: {excel_sheet}")
cells: Range = cached_excels[excel_path][1].sheets[excel_sheet].range(excel_range)
result = (True, extract_func(cells))
else:
raise ValueError(f"Invalid request format: [{request}]")
except Exception:
result = (False, traceback.format_exc())
return result
def check_input_num_core(value: str) -> str:
value = value.strip().lower()
specified_cpu_count = int(value)
max_cpu_count = cpu_count()
if (specified_cpu_count < 0):
raise ArgumentTypeError(f"Number of cores invalid. [{specified_cpu_count}]")
if (specified_cpu_count == 0):
specified_cpu_count = max_cpu_count
if specified_cpu_count > max_cpu_count:
raise ArgumentTypeError(f"Max CPUs allowed: [{max_cpu_count}]. (Demanded: [{specified_cpu_count}])")
return value
def check_input_cell_mode(value: str) -> str:
value = value.strip().lower()
if value not in {"value_fast", "value_aggressive", "formula"}:
raise ArgumentTypeError(f"Cell value mode invalid. [{value}]")
return value
if __name__ == "__main__":
pool: Optional[Pool] = None
# From Console.
specified_cpu_count = 1
try:
# Process input arguments.
parser = ArgumentParser()
parser.add_argument("-num_core", dest="num_core", default="0", help="Number of cores for parallelism.",
metavar="a positive number", type=check_input_num_core)
parser.add_argument("-cell_mode", dest="cell_mode", default="fast", help="The mode to extract value in cell.",
metavar="value_fast|value_aggressive|formula", type=check_input_cell_mode)
inputs = parser.parse_args()
specified_cpu_count = int(inputs.num_core)
cell_value_mode = inputs.cell_mode
extract_func = {
"value_fast": extract_cell_values,
"value_aggressive": partial(extract_cell_property_by_enumerating, extract_func=get_cell_value),
"formula": extract_cell_formulas
}[cell_value_mode]
with open(r"requests.txt", "r") as input_file:
requests = input_file.read().splitlines()
# Start the pool of Workers (Process).
pool = Pool(specified_cpu_count, initialize, ())
data_cell_values_all_at_once = list(map(lambda request: (request, extract_func), requests))
logging.info("Start processing Excel files.")
results = pool.starmap(extracting_data, data_cell_values_all_at_once)
logging.info("Finish processing Excel files.")
for result in results:
bln, df = result
if bln:
logging.info(f"Collected Dataframe: {df.shape}.")
if df.shape[0] > 1 and df.shape[1] > 0:
logging.info(f"First row: [{'|'.join([df.iloc[1, i] for i in range(df.shape[1])])}]")
else:
logging.warning(df)
logging.info("Done")
except Exception as ex: # Fatal exception.
logging.error(traceback.format_exc())
finally:
if pool:
# Clean up!
pool.starmap(extracting_data, [("EXIT", None) for i in range(0, specified_cpu_count)])
pool.close()