Oracle 批绑定(batch bind)FORALL, BULK COLLECT

本文介绍Oracle PL/SQL中FORALL与BULK COLLECT的使用方法及注意事项,包括性能提升、异常处理、限制条件等内容。
FORALL 用法小结: 
  本文主要翻译、整理了ORACLE官方文档上有关FORALL的部份内容,不妥之处,还希望多和大家交流。 
在发送语句到SQL引擎前,FORALL语句告知PL/SQL 引擎批挷定输入集合。尽管FORALL语句包含一个迭代(iteration)模式,它并不一是个FOR循环。其语法为: 
  FORALL index IN lower_bound..upper_bound sql_statement; 


一、如何使用批挷定提高性能(How Do Bulk Binds Improve Performance) 
  在PL/SQL 和SQL引擎(engines)中,太多的上下文切换(context switches)会影响性能。这个会发生在当一个循环为集合中的每个元素执行一个单个SQL语句时。而使用批挷定能显著提高性能。下图显示PL/SQL引擎 和SQL引擎之间的context switches:(PL/SQL引擎执行存过语句仅发送SQL语句到SQL引擎,SQL引擎执行语句后返回数据给PL/SQL引擎) 
  PL/SQL引擎发送一次SQL语句给SQL引擎,在SQL引擎中则为范围中每个index数字执行一次SQL语句。 
  PL/SQL挷定操作包含以下三类: 
  in-bind: When a PL/SQL variable or host variable is stored in the database by an INSERT or UPDATE statement. 
  out-bind:When a database value is assigned to a PL/SQL variable or a host variable by the RETURNING clause of an INSERT, UPDATE, or DELETE statement. 
  define: When a database value is assigned to a PL/SQL variable or a host variable by a SELECT or FETCH statement. 
  在SQL语句中,为PL/SQL变量指定值称为挷定(binding), 
  DML语句能传递所有集合元素到一个单个操作中,这过程称为批挷定(bulk binding)。 
  如果集合有20个元素,批挷定让你用单个操作等效于执行与20个SELECT,INSERT,   UPDATE或DELETE语句。这个技术通过减少在PL/SQL和SQL引擎(engines)间的上下文切换来提高性能。批挷定包括: 
  1.带INSERT, UPDATE, and DELETE语句的批挷定:在FORALL语句中嵌入SQL语句 
  2.带SELECT语句的批挷定:在SELECT语句中用BULK COLLECT 语句代替INTO 
  下边的例子分别用FOR和FORALL进行数据插入,以显示用批挷定的对性能的提高: 


Java代码  收藏代码
  1. SQL> SET SERVEROUTPUT ON  
  2. SQL> CREATE TABLE parts (pnum NUMBER(4), pname CHAR(15));  
  3. Table created.  
  4. SQL> DECLARE  
  5. 2 TYPE NumTab IS TABLE OF parts.pnum%TYPE INDEX BY BINARY_INTEGER;  
  6. 3 TYPE NameTab IS TABLE OF parts.pname%TYPE INDEX BY BINARY_INTEGER;  
  7. 4 pnums NumTab;  
  8. 5 Pnames NameTab;  
  9. 6 t1 NUMBER;  
  10. 7 t2 NUMBER;  
  11. 8 t3 NUMBER;  
  12. 9 BEGIN  
  13. 10 FOR i IN 1..500000 LOOP  
  14. 11 pnums(i) := i;  
  15. 12 pnames(i) := 'Part No.'||to_char(i);  
  16. 13 END LOOP;  
  17. 14 t1 := dbms_utility.get_time;  
  18. 15  
  19. 16 FOR i IN 1..500000 LOOP  
  20. 17 INSERT INTO parts VALUES(pnums(i),pnames(i));  
  21. 18 END LOOP;  
  22. 19 t2 := dbms_utility.get_time;  
  23. 20  
  24. 21 FORALL i IN 1..500000  
  25. 22 INSERT INTO parts VALUES(pnums(i),pnames(i));  
  26. 23 t3 := dbms_utility.get_time;  
  27. 24  
  28. 25 dbms_output.put_line('Execution Time (secs)');  
  29. 26 dbms_output.put_line('---------------------');  
  30. 27 dbms_output.put_line('FOR loop: ' || TO_CHAR(t2 - t1));  
  31. 28 dbms_output.put_line('FORALL: ' || TO_CHAR(t3 - t2));  
  32. 29 END;  

SQL> / 
Execution Time (secs) 
--------------------- 
FOR loop: 2592 
FORALL: 358 
PL/SQL procedure successfully completed 
  从而可以看出FORALL语句在性能上有显著提高。 
  注释:SQL语句能涉及多个集合,然而,性能提高只适用于下标集合(subscripted collections) 
二、FORALL 如何影响回滚(How FORALL Affects Rollbacks) 
  在FORALL语句中,如果任何SQL语句执行产生未处理的异常(exception),先前执行的所有数据库改变都会被回滚。然而,如果产生的异常被捕获并处理,则回滚改变到一个隐式的保存点,该保存点在每个SQL语句执行前被标记。之前的改变不会被回滚。例如: 


Java代码  收藏代码
  1. CREATE TABLE emp2 (deptno NUMBER(2), job VARCHAR2(15));  
  2. INSERT INTO emp2 VALUES(10'Clerk');  
  3. INSERT INTO emp2 VALUES(10'Clerk');  
  4. INSERT INTO emp2 VALUES(20'Bookkeeper'); -- 10-char job title  
  5. INSERT INTO emp2 VALUES(30'Analyst');  
  6. INSERT INTO emp2 VALUES(30'Analyst');  
  7. Comit;  
  8. DECLARE  
  9. TYPE NumList IS TABLE OF NUMBER;  
  10. depts NumList := NumList(102030);  
  11. BEGIN  
  12. FORALL j IN depts.FIRST..depts.LAST  
  13. UPDATE emp2 SET job = job || ' (temp)'  
  14. WHERE deptno = depts(j);  
  15. -- raises a "value too large" exception  
  16. EXCEPTION  
  17. WHEN OTHERS THEN  
  18. COMMIT;  
  19. END;  
  20. /  
  21. PL/SQL procedure successfully completed  
  22. SQL> select * from emp2;  

DEPTNO JOB 
---------- --------------- 
10 Clerk temp 
10 Clerk temp 
20 Bookkeeper 
30 Analyst 
30 Analyst 
  上边的例子SQL引擎执行UPDATE语句3次,指定范围内的每个索引号一次。第一个(depts(10))执行成功,但是第二个(depts(20))执行失败(插入值超过了列长),因此,仅仅第二个执行被回滚。 
  当执行任何SQL语句引发异常时,FORALL语句中断(halt)。上边的例子中,执行第二个UPDATE语句引发异常, 因此第三个语句不会执行。 
三、用%BULK_ROWCOUNT 属性计算FORALL迭代影响行数 
  在进行SQL数据操作语句时,SQL引擎打开一个隐式游标(命名为SQL),该游标的标量属性(scalar attribute)有 %FOUND, %ISOPEN, %NOTFOUND, and %ROWCOUNT。 
  FORALL语句除具有上边的标量属性外,还有个复合属性(composite attribute):%BULK_ROWCOUNT,该属性具有索引表(index-by table)语法。它的第i个元素存贮SQL语句(INSERT, UPDATE或DELETE)第i个执行的处理行数。如果第i个执行未影响行,%bulk_rowcount (i),返回0。FORALL与%bulk_rowcount属性使用相同下标。例如: 


DECLARE 
TYPE NumList IS TABLE OF NUMBER; 
depts NumList := NumList(10, 20, 50); 
BEGIN 
FORALL j IN depts.FIRST..depts.LAST 
UPDATE emp SET sal = sal * 1.10 WHERE deptno = depts(j); 
-- Did the 3rd UPDATE statement affect any rows? 
IF SQL%BULK_ROWCOUNT(3) = 0 THEN ... 
END; 
  %ROWCOUNT 返回SQL语句所有执行处理总的行数 
  %FOUND和 %NOTFOUND仅与SQL语句的最后执行有关,但是,可以使用%BULK_ROWCOUNT推断单个执行的值,如%BULK_ROWCOUNT(i)为0时,%FOUND和%NOTFOUND分别是FALSE和TRUE。 
四、用%BULK_EXCEPTIONS属性处理FORALL异常 
  在执行FORALL语句期间,PL/SQL提供一个处理异常的机制。该机制使批挷定(bulk-bind)操作能保存异常信息并继续执行。方法是在FORALL语句中增加SAVE EXCEPTIONS关键字。语法为: 
  FORALL index IN lower_bound..upper_bound SAVE EXCEPTIONS 
    {insert_stmt | update_stmt | delete_stmt} 
  执行期间引发的所有异常都被保存游标属性 %BULK_EXCEPTIONS中,它存贮一个集合记录,每记录有两个字段: 
  %BULK_EXCEPTIONS(i).ERROR_INDEX:存贮在引发异常期间FORALL语句迭代(重复:iteration) 
  %BULK_EXCEPTIONS(i).ERROR_CODE:存贮相应的Oracle错误代码 
  %BULK_EXCEPTIONS.COUNT存贮异常的数量。(该属性不是%BULK_EXCEPTIONS集合记录的字段)。如果忽略SAVE EXCEPTIONS,当引发异常时,FORALL语句停止执行。此时,SQL%BULK_EXCEPTIONS.COUNT 返回1, 且SQL%BULK_EXCEPTIONS只包含一条记录。如果执行期间无异常 SQL%BULK_EXCEPTIONS.COUNT 返回 0.例子: 


DECLARE 
TYPE NumList IS TABLE OF NUMBER; 
num_tab NumList := NumList(10,0,11,12,30,0,20,199,2,0,9,1); 
errors NUMBER; 
dml_errors EXCEPTION; 
PRAGMA exception_init(dml_errors, -24381); 
BEGIN 
FORALL i IN num_tab.FIRST..num_tab.LAST SAVE EXCEPTIONS 
DELETE FROM emp WHERE sal > 500000/num_tab(i); 
EXCEPTION 
WHEN dml_errors THEN 
errors := SQL%BULK_EXCEPTIONS.COUNT; 
dbms_output.put_line('Number of errors is ' || errors); 
FOR i IN 1..errors LOOP 
dbms_output.put_line('Error ' || i || ' occurred during '|| 
'iteration ' || SQL%BULK_EXCEPTIONS(i).ERROR_INDEX); 
dbms_output.put_line('Oracle error is ' || 
SQLERRM(-SQL%BULK_EXCEPTIONS(i).ERROR_CODE)); 
END LOOP; 
END; 
该例子中,当i等于2,6,10时,产生异常ZERO_DIVIDE,完成后SQL%BULK_EXCEPTIONS.COUNT为3,其值为(2,1476), (6,1476)和(10,1476),错误输出如下: 
  Number of errors is 3 
  Error 1 occurred during iteration 2 
  Oracle error is ORA-01476: divisor is equal to zero 
  Error 2 occurred during iteration 6 
  Oracle error is ORA-01476: divisor is equal to zero 
  Error 3 occurred during iteration 10 
  Oracle error is ORA-01476: divisor is equal to zero 
五、用BULK COLLECT子句取回查询结果至集合中 
  在返回到PL/SQL引擎之前,关键字BULK COLLECT告诉SQL引擎批挷定输出集合。该关键字能用于SELECT INTO, FETCH INTO和RETURNING INTO语句中。语法如下: 
  ... BULK COLLECT INTO collection_name[, collection_name] ... 


示例1: 
DECLARE 
TYPE NumTab IS TABLE OF emp.empno%TYPE; 
TYPE NameTab IS TABLE OF emp.ename%TYPE; 
enums NumTab; -- no need to initialize 
names NameTab; 
BEGIN 
SELECT empno, ename BULK COLLECT INTO enums, names FROM emp; 
... 
END; 
示例2: 
CREATE TYPE Coords AS OBJECT (x NUMBER, y NUMBER); 
CREATE TABLE grid (num NUMBER, loc Coords); 
INSERT INTO grid VALUES(10, Coords(1,2)); 
INSERT INTO grid VALUES(20, Coords(3,4)); 
DECLARE 
TYPE CoordsTab IS TABLE OF Coords; 
pairs CoordsTab; 
BEGIN 
SELECT loc BULK COLLECT INTO pairs FROM grid; 
-- now pairs contains (1,2) and (3,4) 
END; 
示例3: 
DECLARE 
TYPE SalList IS TABLE OF emp.sal%TYPE; 
sals SalList; 
BEGIN 
SELECT sal BULK COLLECT INTO sals FROM emp 
WHERE ROWNUM <= 100; 
... 
END; 
示例4:Examples of Bulk Fetching from a Cursor: 
DECLARE 
TYPE NameList IS TABLE OF emp.ename%TYPE; 
TYPE SalList IS TABLE OF emp.sal%TYPE; 
CURSOR c1 IS SELECT ename, sal FROM emp WHERE sal > 1000; 
names NameList; 
sals SalList; 
BEGIN 
OPEN c1; 
FETCH c1 BULK COLLECT INTO names, sals; --可返回到一个或多个集合 
END; 
示例5:Examples of Bulk Fetching from a Cursor: 
DECLARE 
TYPE DeptRecTab IS TABLE OF dept%ROWTYPE; 
dept_recs DeptRecTab; 
CURSOR c1 IS 
SELECT deptno, dname, loc FROM dept WHERE deptno > 10; 
BEGIN 
OPEN c1; 
FETCH c1 BULK COLLECT INTO dept_recs; --返回到一个记录(records)集合 
END; 
六、用LIMIT 子句限制批取出操作行 
  LIMIT子句可限制从数据库中取出的行数。该子句仅能用于批(非标量 not scalar)FETCH语句.语法为: 
  FETCH ... BULK COLLECT INTO ... [LIMIT rows]; 
  rows可以是文字的(literal),变量(variable)或表达式(expression),但必须返回一个数字。否则,PL/SQL引发预定义异常VALUE_ERROR,如果为负数,PL/SQ引发INVALID_NUMBER。如果需要,PL/SQL四舍五入(round)为最近的整数. 
  下例中,在每个循环迭代中,FETCH语句返回到索引表empnos中,先前的值会被覆盖。 


DECLARE 
TYPE NumTab IS TABLE OF NUMBER INDEX BY BINARY_INTEGER; 
CURSOR c1 IS SELECT empno FROM emp; 
empnos NumTab; 
rows NATURAL := 10; 
BEGIN 
OPEN c1; 
LOOP 
/**//* The following statement fetches 10 rows (or less). */ 
FETCH c1 BULK COLLECT INTO empnos LIMIT rows; 
EXIT WHEN c1%NOTFOUND; 
... 
END LOOP; 
CLOSE c1; 
END; 
七、用RETURNING INTO子句取回DML结果至集合 
  你能在INSERT、UPDATE或DELETE语句的RETURNING INTO子句中使用BULK COLLECT子句。(注意,此处是没有SELECT语句的) 


DECLARE 
TYPE NumList IS TABLE OF emp.empno%TYPE; 
enums NumList; 
BEGIN 
DELETE FROM emp WHERE deptno = 20 
RETURNING empno BULK COLLECT INTO enums; 
-- if there were five employees in department 20, 
-- then enums contains five employee numbers 
END; 
八、BULK COLLECT上的限制 
  1.You cannot bulk collect into an associative array that has a string type for the key. . 
  2.BULK COLLECT语句只能用于服务器端(server-side),而非客户端 
  3.在BULK COLLECT INTO语句中的所有目标必须是集合,如下例: 


DECLARE 
TYPE NameList IS TABLE OF emp.ename%TYPE; 
names NameList; 
salary emp.sal%TYPE; 
BEGIN 
SELECT ename, sal BULK COLLECT INTO names, salary -- illegal target 
FROM emp WHERE ROWNUM < 50; 
... 
END;. 
  4.复合目标(如对象)不能用于RETURNING INTO子句中. 
  5.当需要隐式数据类型转换时,多复合目标(如对象集合)不能用于bulk collect into子句。 
九、同时使用FORALL 和BULK COLLECT 
  Using FORALL and BULK COLLECT Together 
  你能使FORALL语句与BULK COLLECT结合,如下例:如果集合depts有3个元素,每个元素导致5行被删除,则语句完成时,集合enums有15个元素: 
  FORALL j IN depts.FIRST..depts.LAST 
  DELETE FROM emp WHERE empno = depts(j) 
  RETURNING empno BULK COLLECT INTO enums; 
  注意:不能在FORALL语句中使用SELECT ... BULK COLLECT语句。 
十、Using Host Arrays with Bulk Binds 
  客户端程序能用匿名PL/SQL块批挷定输入和输出数组(arrays)。实际上,这是与服务器端传递集合最有效的方式。 
  Host arrays are declared in a host environment such as an OCI or Pro*C program and must be prefixed with a colon to distinguish them from PL/SQL collections. In the example below, an input host array is used in a DELETE statement. At run time, the anonymous PL/SQL block is sent to the database server for execution. 
  DECLARE 
  ... 
  BEGIN 
  -- assume that values were assigned to the host array 
  -- and host variables in the host environment 
  FORALL i IN :lower..:upper 
  DELETE FROM emp WHERE deptno = :depts(i); 
  ... 
  END; 
  限制:以下限制将应用于FORALL语句: 
  1. 在FORALL循环中,UPDATE语句中的SET子句和WHERE子句中不能指向同一个集合,你可能需要获得另一个集合副本,以在WHERE子句指向新的名称。 
  2. INSERT,UPDATE或DELETE语句必须至少涉及一个集合。如在LOOP插入一组常量的FORALL语句将引发异常。( FORALL的INSERT之类的语句一定要用in-bind 方式,如: 


SQL> BEGIN 
2 FORALL i IN 1..100 
3 INSERT INTO parts VALUES(i,i); 
4 END; 
5 / 
ORA-06550: 第 3 行, 第 5 列: 
PLS-00435: 没有 BULK In-BIND 的 DML 语句在 FORALL 内不能使用 
ORA-06550: 第 2 行, 第 12 列: 
PL/SQL: Statement ignored 
  3. 指定范围内的所有集合元素必须存在,如果一元素丢失或删除,你将收到一个错误,如: 


DECLARE 
TYPE NumList IS TABLE OF NUMBER; 
depts NumList := NumList(10, 20, 30, 40); 
BEGIN 
depts.DELETE(3); -- delete third element 
FORALL i IN depts.FIRST..depts.LAST 
DELETE FROM emp WHERE deptno = depts(i); -- causes an error 
END; 
  4. 下例显示,复合值的输入集合不能被分解和跳跃数据库列 


CREATE TABLE coords (x NUMBER, y NUMBER); 
CREATE TYPE Pair AS OBJECT (m NUMBER, n NUMBER); 
DECLARE 
TYPE PairTab IS TABLE OF Pair; 
pairs PairTab := PairTab(Pair(1,2), Pair(3,4), Pair(5,6)); 
TYPE NumTab IS TABLE OF NUMBER; 
nums NumTab := NumTab(1, 2, 3); 
BEGIN 
/**//* The following statement fails. */ 
FORALL i IN 1..3 
UPDATE coords SET (x, y) = pairs(i) 
WHERE x = nums(i); 
END; 
The workaround is to decompose the composite values manually: 
DECLARE 
TYPE PairTab IS TABLE OF Pair; 
pairs PairTab := PairTab(Pair(1,2), Pair(3,4), Pair(5,6)); 
TYPE NumTab IS TABLE OF NUMBER; 
nums NumTab := NumTab(1, 2, 3); 
BEGIN 
/**//* The following statement succeeds. */ 
FORALL i in 1..3 
UPDATE coords SET (x, y) = (pairs(i).m, pairs(i).n) 
WHERE x = nums(i); 
END; 
  5. 集合下标不能是表达式,示例: 
  FORALL j IN mgrs.FIRST..mgrs.LAST 
  DELETE FROM emp WHERE mgr = mgrs(j+1); -- invalid subscript 
  6. 游标属性%BULK_ROWCOUNT不能分配给其它集合,同样,它也不能作为参数传递到子程序。
import os import re import sys import tkinter as tk from tkinter import ttk, Frame, Scrollbar, Canvas from tkinter import scrolledtext, messagebox import pandas as pd import numpy as np from glob import glob import seaborn as sns from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk import matplotlib.ticker as ticker import matplotlib.pyplot as plt from matplotlib.font_manager import FontProperties from matplotlib.figure import Figure plt.switch_backend("TkAgg") plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"] plt.rcParams["axes.unicode_minus"] = False def find_batch_folders(path, batch_number): target_dir = "4WCSVLog" # 需筛选的上级目录名 batch_folders = [] for root, dirs, files in os.walk(path): path_components = os.path.normpath(root).split(os.sep) if target_dir in path_components and batch_number in dirs: full_path = os.path.join(root, batch_number) batch_folders.append(full_path) return batch_folders def get_csv_files_path1(folder): all_files = glob(os.path.join(folder, "*-SVP1B*.csv")) files = [f for f in all_files if "HEAD" not in f] return files def get_csv_files_path2(folder): all_files = glob(os.path.join(folder, "*-SVP1B*.csv")) files = all_files return files def parse_svp1b(filename): match = re.search(r"svp1b([0-9A-Z])([0-9])([0-9])", filename, re.IGNORECASE) if match: piece, line, file_num = match.groups() if piece.isalpha(): piece_num = 10 + ord(piece.upper()) - ord("A") else: piece_num = int(piece) return piece_num, int(line), int(file_num) return None def read_csv_file(file, start_row=0, start_col=0): df = pd.read_csv(file, header=None, skiprows=start_row) return df.iloc[:, start_col:] if start_col > 0 else df def merge_files(files, path_type): piece_dict = {} for file in files: svp_info = parse_svp1b(os.path.basename(file).lower()) if not svp_info: continue piece, line, file_num = svp_info if path_type == 1: df = read_csv_file(file, start_row=2) else: df = read_csv_file(file, start_row=4, start_col=18) if df.empty: continue key = (piece, line) if key not in piece_dict: piece_dict[key] = [] piece_dict[key].append((file_num, df)) sorted_keys = sorted(piece_dict.keys(), key=lambda x: (x[0], x[1])) global_col_counter = 1 final_dfs = [] for key in sorted_keys: file_dfs = piece_dict[key] file_dfs.sort(key=lambda x: x[0]) base_df = None for file_num, df in file_dfs: if base_df is None: global_col_counter = 1 new_columns = [] for _ in df.columns: new_columns.append(f"net{global_col_counter}") global_col_counter += 1 df.columns = new_columns base_df = df else: new_columns = [] for _ in df.columns: new_columns.append(f"net{global_col_counter}") global_col_counter += 1 df.columns = new_columns base_df = pd.concat([base_df, df], axis=1) # 横向拼接 if base_df is not None: final_dfs.append(base_df) return ( pd.concat(final_dfs, axis=0, ignore_index=True) if final_dfs else pd.DataFrame() ) def collect_batch_data(batch_list): batch_data_dict = {} path1 = r"\\10.127.1.248\c1tst\7750" path2 = r"\\10.127.1.248\c1tst\7755" for idx, batch_number in enumerate(batch_list, 1): print(f"=== 处理第 {idx}/{len(batch_list)} 个次:{batch_number} ===") batch_data = pd.DataFrame() folders1 = find_batch_folders(path1, batch_number) if folders1: for folder1 in folders1: files = get_csv_files_path1(folder1) current_data = ( merge_files(files, path_type=1) if files else pd.DataFrame() ) batch_data = pd.concat( [batch_data, current_data], axis=0, ignore_index=True ) print(f"从路径1找到 {len(files)} 个数据文件") else: folders2 = find_batch_folders(path2, batch_number) if folders2: for folder2 in folders2: files = get_csv_files_path2(folder2) current_data = ( merge_files(files, path_type=2) if files else pd.DataFrame() ) batch_data = pd.concat( [batch_data, current_data], axis=0, ignore_index=True ) print(f"从路径2找到 {len(files)} 个数据文件") else: print(f"次 {batch_number}:未找到对应文件夹,跳过\n") continue if not batch_data.empty: batch_data_dict[batch_number] = batch_data print(f"次 {batch_number} 数据合并完成,共 {len(batch_data)} 行\n") else: print(f"次 {batch_number}:无有效数据,跳过\n") return batch_data_dict class RedirectText: def __init__(self, text_widget): self.text_widget = text_widget def write(self, string): self.text_widget.insert(tk.END, string) self.text_widget.see(tk.END) def flush(self): pass def plot_all_nets(batch_data_dict, batch_list, net_list): """在一个窗口中显示所有NET的图表,支持滚动""" if not batch_data_dict: print("无有效次数据,无法绘图") return # 创建主图表窗口 plot_window = tk.Toplevel(root) plot_window.title(f"次数据分布箱线图") plot_window.geometry("1200x800") # 创建滚动框架 scroll_frame = Frame(plot_window) scroll_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 添加垂直滚动条 vscrollbar = Scrollbar(scroll_frame, orient=tk.VERTICAL) vscrollbar.pack(side=tk.RIGHT, fill=tk.Y) # 创建画布用于滚动 canvas = Canvas(scroll_frame, yscrollcommand=vscrollbar.set) canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) vscrollbar.config(command=canvas.yview) # 创建内容框架(放在画布上) content_frame = Frame(canvas) canvas_frame = canvas.create_window((0, 0), window=content_frame, anchor="nw") # 绑定配置事件以更新滚动区域 def on_frame_configure(event): canvas.configure(scrollregion=canvas.bbox("all")) content_frame.bind("<Configure>", on_frame_configure) # 绑定鼠标滚轮事件 def on_mousewheel(event): canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") canvas.bind_all("<MouseWheel>", on_mousewheel) # 创建所有NET的图表 row_count = 0 for net in net_list: # 收集当前NET在所有次的数值 net_data = pd.DataFrame() for batch in batch_list: if batch in batch_data_dict and net in batch_data_dict[batch].columns: net_data[f" {batch}"] = batch_data_dict[batch][net] if net_data.empty: continue # 创建图表框架 frame = Frame(content_frame, bd=2, relief=tk.GROOVE, padx=10, pady=10) frame.grid(row=row_count, column=0, sticky="nsew", padx=5, pady=5) row_count += 1 # 添加NET标签 tk.Label(frame, text=f"NET: {net}", font=("SimHei", 12, "bold")).pack( anchor=tk.W ) # 创建图表 fig = Figure(figsize=(10, 4), dpi=100) ax = fig.add_subplot(111) # 准备数据 long_data = net_data.melt(var_name="Lot", value_name="测量值") long_data["次序号"] = long_data["Lot"].str.extract(r"(\d+)").astype(int) long_data = long_data.sort_values("次序号") # 绘制箱线图 sns.boxplot( x="Lot", y="测量值", data=long_data, ax=ax, boxprops=dict(facecolor="lightblue", alpha=0.7), medianprops=dict(color="red", linewidth=2), ) # 设置图表样式 ax.set_title(f"{net} ", fontsize=12) ax.set_xlabel("", fontsize=10) ax.set_ylabel("Value", fontsize=10) ax.tick_params(axis="x", labelsize=8, rotation=30) ax.grid(axis="y", linestyle="--", alpha=0.7) fig.tight_layout() # 标注统计值 all_values = long_data["测量值"].dropna() if not all_values.empty: data_range = all_values.max() - all_values.min() for i, col in enumerate(net_data.columns): col_values = net_data[col].dropna() if col_values.empty: continue q1 = col_values.quantile(0.25) median = col_values.median() q3 = col_values.quantile(0.75) text_offset = data_range * 0.03 ax.text(i, q1 - text_offset, f"Q1: {q1:.2f}", ha="center", fontsize=8) ax.text( i, median, f"Med: {median:.2f}", ha="center", fontsize=8, color="red", ) ax.text(i, q3 + text_offset, f"Q3: {q3:.2f}", ha="center", fontsize=8) # 嵌入图表到Tkinter canvas_plot = FigureCanvasTkAgg(fig, master=frame) canvas_plot.draw() canvas_plot.get_tk_widget().pack(fill=tk.X) # 添加工具栏(提供缩放等功能) toolbar = NavigationToolbar2Tk(canvas_plot, frame) toolbar.update() canvas_plot.get_tk_widget().pack(fill=tk.X) # 添加分隔线 ttk.Separator(content_frame, orient=tk.HORIZONTAL).grid( row=row_count, column=0, sticky="ew", pady=10 ) row_count += 1 # 如果没有有效图表 if row_count == 0: tk.Label(content_frame, text="没有找到有效的NET数据", font=("SimHei", 12)).grid( row=0, column=0 ) print("没有找到有效的NET数据") # 添加关闭按钮 close_btn = tk.Button( plot_window, text="关闭窗口", command=plot_window.destroy, bg="#f44336", fg="white", font=("SimHei", 10), ) close_btn.pack(pady=10) def process_batches(): input_text = batch_entry.get().strip() if not input_text: messagebox.showwarning("输入警告", "请输入次号后再处理!") return batch_list = re.split(r"[,\s]+", input_text) batch_list = [b for b in batch_list if b] if not batch_list: messagebox.showwarning("输入警告", "未识别到有效次号!") return # 解析NET输入 net_text = net_entry.get().strip() net_list = [] if net_text: raw_nets = re.split(r"[,\s]+", net_text) raw_nets = [n.strip() for n in raw_nets if n.strip()] for net in raw_nets: if net.startswith("net") and net[3:].isdigit(): net_list.append(net) elif net.isdigit(): net_list.append(f"net{net}") else: print(f"警告:无效NET格式 '{net}',已忽略") net_list = list(dict.fromkeys(net_list)) net_list.sort( key=lambda x: int(x[3:]) ) # x是"netX"格式,x[3:]提取数字字符串并转为int print(f"已解析NET列表(排序后):{', '.join(net_list)}") else: net_list = None process_btn.config(state=tk.DISABLED) log_text.delete(1.0, tk.END) print(f"开始处理次:{', '.join(batch_list)}\n") # 收集所有次的数据 batch_data_dict = collect_batch_data(batch_list) # 收集所有NET的数据 net_data_dict = {} if net_list: for net in net_list: part_data = pd.DataFrame() for batch_number in batch_list: if batch_number in batch_data_dict: if net in batch_data_dict[batch_number].columns: part_data[batch_number] = batch_data_dict[batch_number][net] print(f"从次 {batch_number} 中提取 {net} 数据") else: print(f"警告:NET {net} 在次 {batch_number} 中不存在,已忽略") else: print(f"警告:次 {batch_number} 的数据未找到,已忽略") if not part_data.empty: net_data_dict[net] = part_data else: print("未指定NET编号,无法绘图") # 在一个窗口中显示所有NET if net_data_dict: plot_all_nets(batch_data_dict, batch_list, net_list) else: print("所有NET均无有效数据,无法绘图") process_btn.config(state=tk.NORMAL) print("所有次处理完成!") def create_gui(): global root, batch_entry, log_text, process_btn, net_entry root = tk.Tk() root.title("電測阻值箱線圖") root.geometry("800x650") root.resizable(True, True) # 次号输入区域 input_frame = tk.Frame(root, padx=10, pady=10) input_frame.pack(fill=tk.X) tk.Label( input_frame, text="次号输入(多个次用逗号/空格分隔):", font=("SimHei", 10) ).pack(side=tk.LEFT) batch_entry = tk.Entry(input_frame, width=50, font=("SimHei", 10)) batch_entry.pack(side=tk.LEFT, padx=5) batch_entry.insert(0, "25AL90070800,25AL90070400") # NET编号输入 net_frame = tk.Frame(root, padx=10, pady=5) net_frame.pack(fill=tk.X) tk.Label( net_frame, text="NET编号输入(多个用逗号/空格分隔,留空显示全部):", font=("SimHei", 10), ).pack(side=tk.LEFT) net_entry = tk.Entry(net_frame, width=50, font=("SimHei", 10)) net_entry.pack(side=tk.LEFT, padx=5) net_entry.insert(0, "1,2,3,4,5") # 測試先默認 # 按钮 btn_frame = tk.Frame(root, padx=10, pady=5) btn_frame.pack(fill=tk.X) process_btn = tk.Button( btn_frame, text="开始处理", command=process_batches, bg="#4CAF50", fg="white", font=("SimHei", 10), ) process_btn.pack(side=tk.LEFT, padx=5) clear_btn = tk.Button( btn_frame, text="清空日志", command=lambda: log_text.delete(1.0, tk.END), bg="#f44336", fg="white", font=("SimHei", 10), ) clear_btn.pack(side=tk.LEFT, padx=5) # 日志 log_frame = tk.Frame(root, padx=10, pady=5) log_frame.pack(fill=tk.BOTH, expand=True) tk.Label(log_frame, text="处理日志:", font=("SimHei", 10)).pack(anchor=tk.W) log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, font=("SimHei", 9)) log_text.pack(fill=tk.BOTH, expand=True) sys.stdout = RedirectText(log_text) root.mainloop() if __name__ == "__main__": create_gui() 我現有的程式如上,你給我提供一個完整的代碼
最新发布
11-28
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值