【Java】基于 Tabula 的 PDF 合并单元格内容提取

作者:Kida的躺平小屋日期:2025/10/22

坑还是要填的,但是填得是否平整就有待商榷了(狗头保命...)。

本人技术有限,只能帮各位实现的这个地步了。各路大神如果还有更好的实现也可以发出来跟小弟共勉一下哈。

首先需要说一下的是以下提供的代码仅作研究参考使用,各位在使用之前务必自检,因为并不是所有 pdf 的表格格式都适合。


本次实现的难点在于 PDF 是一种视觉格式,而不是语义格式。


它只记录了“在 (x, y) 坐标绘制文本 'ABC'”和“从 (x1, y1) 到 (x2, y2) 绘制一条线”。它根本不“知道”什么是“表格”、“行”或“合并单元格”。而 Tabula 的 SpreadsheetExtractionAlgorithm 算法是处理这种问题的最佳起点,但它提取的结果会是“不规则”的,即每行的单元格数量可能不同。因此本次将采用后处理的方式进行解析,Tabula 更多的只是作内容提取,表格组织还是在后期处理进行的。

就像上次的文章中说到

【Java】采用 Tabula 技术对 PDF 文件内表格进行数据提取

本次解决问题的核心思路就是通过计算每一个单元格完整的边界框,得到它的 top,left, bottom,right。通过收集所有单元格的 top 坐标和 bottom 坐标,推断出表格中所有“真实”的行边界。同理,通过收集所有单元格的 left 坐标和 right 坐标,可以推断出所有“真实”的列边界。最后基于这些边界构建一个完整的网格,然后将 Tabula 提取的文本块“放”回这个网格中。

为了方便测试我使用了 Deepseek 官网“模型细节”章节里面的那个表格。

image.png

这个表格是比较经典的,既有列合并单元格,也有行合并单元格。而且表格中并没有那么多复杂的内容。

下面是我的执行代码

1package cn.paohe;
2
3import java.awt.Point;
4import java.io.BufferedInputStream;
5import java.io.File;
6import java.io.FileInputStream;
7import java.io.IOException;
8import java.io.InputStream;
9import java.util.ArrayList;
10import java.util.HashSet;
11import java.util.List;
12import java.util.NavigableSet;
13import java.util.Set;
14import java.util.TreeSet;
15
16import org.apache.pdfbox.pdmodel.PDDocument;
17
18import technology.tabula.ObjectExtractor;
19import technology.tabula.Page;
20import technology.tabula.PageIterator;
21import technology.tabula.Rectangle;
22import technology.tabula.RectangularTextContainer;
23import technology.tabula.Table;
24import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;
25
26public class MergedCellPdfExtractor {
27
28    // 浮点数比较的容差
29    private static final float COORDINATE_TOLERANCE = 2.0f;
30
31    /**
32	 * 为了样例方便,在类内部直接封装一个单元格,包含其文本和边界
33	 */
34    static class Cell extends Rectangle {
35        public String text;
36        public float top;
37        public float left;
38        public double width;
39        public double height;
40
41        public Cell(float top, float left, double width, double height, String text) {
42            this.top = top;
43            this.left = left;
44            this.width = width;
45            this.height = height;
46            this.text = text == null ? "" : text.trim();
47        }
48
49        @Override
50        public float getTop() {
51            return top;
52        }
53
54        @Override
55        public float getLeft() {
56            return left;
57        }
58
59        @Override
60        public double getWidth() {
61            return width;
62        }
63
64        @Override
65        public double getHeight() {
66            return height;
67        }
68
69        @Override
70        public float getBottom() {
71            return (float) (top + height);
72        }
73
74        @Override
75        public float getRight() {
76            return (float) (left + width);
77        }
78
79        @Override
80        public double getX() {
81            return left;
82        }
83
84        @Override
85        public double getY() {
86            return top;
87        }
88
89        @Override
90        public Point[] getPoints() {
91            return new Point[0];
92        }
93
94        @Override
95        public String toString() {
96            return String.format("Cell[t=%.2f, l=%.2f, w=%.2f, h=%.2f, text='%s']", top, left, width, height, text);
97        }
98    }
99
100    /**
101	 * 由于表格提取的时候会出现偏差,因此定义表格指纹,用于去重
102	 */
103    static class TableFingerprint {
104        private final float top;
105        private final float left;
106        private final int rowCount;
107        private final int colCount;
108        private final String contentHash;
109
110        public TableFingerprint(Table table) {
111            this.top = roundCoordinate(table.getTop());
112            this.left = roundCoordinate(table.getLeft());
113            this.rowCount = table.getRowCount();
114            this.colCount = table.getColCount();
115            this.contentHash = generateContentHash(table);
116        }
117
118        /**
119		 * 生成表格的内容 Hash,用于快速比较两个表格是否相同
120		 * 
121		 * Hash 生成规则:将每个单元格的文本内容连接起来,使用 "|" 分隔 如果单元格的数量超过 10 个,就停止生成 Hash
122		 * 
123		 * @param table 需要生成 Hash 的表格
124		 * @return 生成的 Hash
125		 */
126        private String generateContentHash(Table table) {
127            StringBuilder sb = new StringBuilder();
128            int cellCount = 0;
129            for (List<RectangularTextContainer> row : table.getRows()) {
130                for (RectangularTextContainer cell : row) {
131                    sb.append(cell.getText()).append("|");
132                    cellCount++;
133                    if (cellCount > 10) {
134                        break;
135                    }
136                }
137                if (cellCount > 10) {
138					break;
139				}
140			}
141			return sb.toString();
142		}
143
144		@Override
145		public boolean equals(Object obj) {
146			if (!(obj instanceof TableFingerprint)) {
147				return false;
148			}
149
150			// 将要比较的对象强制转换为 TableFingerprint
151			TableFingerprint other = (TableFingerprint) obj;
152
153			// 两个表格的 top 和 left 坐差不能超过 COORDINATE_TOLERANCE
154			boolean topMatch = Math.abs(this.top - other.top) < COORDINATE_TOLERANCE;
155			boolean leftMatch = Math.abs(this.left - other.left) < COORDINATE_TOLERANCE;
156
157			// 两个表格的行数和列数必须相同
158			boolean rowMatch = this.rowCount == other.rowCount;
159			boolean colMatch = this.colCount == other.colCount;
160
161			// 两个表格的内容 Hash must be equal
162			boolean contentMatch = this.contentHash.equals(other.contentHash);
163
164			// 如果以上条件都满足,则返回 true
165			return topMatch && leftMatch && rowMatch && colMatch && contentMatch;
166		}
167
168		@Override
169		public int hashCode() {
170			return contentHash.hashCode();
171		}
172
173		/**
174		 * 将坐标四舍五入到指定精度,减少浮点误差
175		 * 
176		 * @param coord 需要四舍五入的坐标
177		 * @return 四舍五入后的坐标
178		 */
179		private static float roundCoordinate(float coord) {
180			// 将坐标乘以 10,然后将结果四舍五入,然后除以 10.0f,保留一个小数点
181			return Math.round(coord * 10) / 10.0f;
182		}
183	}
184
185	/**
186	 * 解析 PDF 文件中的所有表格
187	 * 
188	 * 1. 使用 ObjectExtractor 将 PDF 文件中的所有表格进行提取 2. 使用 SpreadsheetExtractionAlgorithm
189	 * 基于线条检测表格,避免重复表格 3. 规范化表格,处理合并单元格
190	 * 
191	 * @param pdfFile 要解析的 PDF 文件
192	 * @return 规范化的表格数据,每个 List<List<String>> 代表一个表格
193	 * @throws IOException 文件读取异常
194	 */
195	public List<List<List<String>>> parseTables(File pdfFile) throws IOException {
196		List<List<List<String>>> allNormalizedTables = new ArrayList<>();
197		Set<TableFingerprint> seenTables = new HashSet<>();
198
199		InputStream bufferedStream = new BufferedInputStream(new FileInputStream(pdfFile));
200		try (PDDocument pdDocument = PDDocument.load(bufferedStream)) {
201			ObjectExtractor oe = new ObjectExtractor(pdDocument);
202			PageIterator pi = oe.extract();
203
204			while (pi.hasNext()) {
205				Page page = pi.next();
206
207				// 使用 SpreadsheetExtractionAlgorithm 基于线条检测表格
208				SpreadsheetExtractionAlgorithm sea = new SpreadsheetExtractionAlgorithm();
209				List<Table> tables = sea.extract(page);
210
211				for (Table table : tables) {
212					// 去重检查
213					TableFingerprint fingerprint = new TableFingerprint(table);
214					if (seenTables.contains(fingerprint)) {
215						System.out.println("跳过重复表格: top=" + fingerprint.top + ", left=" + fingerprint.left);
216						continue;
217					}
218					seenTables.add(fingerprint);
219
220					List<List<String>> normalized = normalizeTable(table);
221					if (!normalized.isEmpty()) {
222						allNormalizedTables.add(normalized);
223					}
224				}
225			}
226		}
227		return allNormalizedTables;
228	}
229
230	/**
231	 * 规范化表格,处理合并单元格
232	 * 
233	 * @param table Tabula 提取的原始表格
234	 * @return 规范化的 List<List<String>>
235	 */
236	private List<List<String>> normalizeTable(Table table) {
237		// 1. 提取所有单元格及其坐标
238		List<Cell> allCells = new ArrayList<>();
239		for (List<RectangularTextContainer> row : table.getRows()) {
240			for (RectangularTextContainer tc : row) {
241				allCells.add(new Cell(tc.getTop(), tc.getLeft(), tc.getWidth(), tc.getHeight(), tc.getText()));
242			}
243		}
244
245		if (allCells.isEmpty()) {
246			return new ArrayList<>();
247		}
248
249		// 2. 收集所有唯一的行起始位置和列起始位置,并添加结束位置
250		NavigableSet<Float> rowBoundaries = new TreeSet<>();
251		NavigableSet<Float> colBoundaries = new TreeSet<>();
252
253		for (Cell cell : allCells) {
254			rowBoundaries.add(roundCoordinate(cell.getTop()));
255			rowBoundaries.add(roundCoordinate(cell.getBottom()));
256			colBoundaries.add(roundCoordinate(cell.getLeft()));
257			colBoundaries.add(roundCoordinate(cell.getRight()));
258		}
259
260		// 3. 转换为列表并去除首尾(表格外边界)
261		List<Float> rowCoords = new ArrayList<>(rowBoundaries);
262		List<Float> colCoords = new ArrayList<>(colBoundaries);
263
264		// 移除最小和最大值(表格外边界),只保留内部网格线
265		if (rowCoords.size() > 2) {
266			rowCoords.remove(rowCoords.size() - 1); // 移除最大值(底边)
267			rowCoords.remove(0); // 移除最小值(顶边)
268		}
269		if (colCoords.size() > 2) {
270			colCoords.remove(colCoords.size() - 1); // 移除最大值(右边)
271			colCoords.remove(0); // 移除最小值(左边)
272		}
273
274		// 4. 验证网格有效性
275		if (rowCoords.isEmpty() || colCoords.isEmpty()) {
276			return tableToListOfListOfStrings(table);
277		}
278
279		int numRows = rowCoords.size();
280		int numCols = colCoords.size();
281		String[][] grid = new String[numRows][numCols];
282
283		// 初始化所有单元格为 null
284		for (int r = 0; r < numRows; r++) {
285			for (int c = 0; c < numCols; c++) {
286				grid[r][c] = null;
287			}
288		}
289
290		// 5. 将单元格内容填充到网格中
291		for (Cell cell : allCells) {
292			// 找到单元格在网格中的起始索引
293			int startRow = findCellStartIndex(rowCoords, cell.getTop());
294			int startCol = findCellStartIndex(colCoords, cell.getLeft());
295
296			// 容错处理
297			if (startRow == -1 || startCol == -1) {
298				continue;
299			}
300
301			// 确保索引有效
302			if (startRow >= numRows || startCol >= numCols) {
303				continue;
304			}
305
306			// 计算单元格跨越的行数和列数
307			int endRow = findCellEndIndex(rowCoords, cell.getBottom());
308			int endCol = findCellEndIndex(colCoords, cell.getRight());
309
310			if (endRow == -1)
311				endRow = numRows - 1;
312			if (endCol == -1)
313				endCol = numCols - 1;
314
315			// 将文本放置在左上角单元格
316			if (grid[startRow][startCol] == null) {
317				grid[startRow][startCol] = cell.text;
318			} else {
319				// 如果已有内容,追加(处理重叠情况)
320				if (!grid[startRow][startCol].isEmpty() && !cell.text.isEmpty()) {
321					grid[startRow][startCol] += " " + cell.text;
322				} else if (!cell.text.isEmpty()) {
323					grid[startRow][startCol] = cell.text;
324				}
325			}
326
327			// 标记被合并覆盖的其他单元格
328			for (int r = startRow; r <= endRow && r < numRows; r++) {
329				for (int c = startCol; c <= endCol && c < numCols; c++) {
330					if (r == startRow && c == startCol) {
331						continue; // 跳过左上角已填充的单元格
332					}
333					if (grid[r][c] == null) {
334						grid[r][c] = ""; // 标记为空字符串(合并单元格的一部分)
335					}
336				}
337			}
338		}
339
340		// 6. 填充空单元格:优先从左侧填充,左侧为空则从上方填充
341		for (int r = 0; r < numRows; r++) {
342			for (int c = 0; c < numCols; c++) {
343				if (grid[r][c] == null || grid[r][c].isEmpty()) {
344					String fillContent = null;
345
346					// 优先从左侧获取内容
347					if (c > 0 && grid[r][c - 1] != null && !grid[r][c - 1].isEmpty()) {
348						fillContent = grid[r][c - 1];
349					}
350					// 左侧为空或不存在,从上方获取内容
351					else if (r > 0 && grid[r - 1][c] != null && !grid[r - 1][c].isEmpty()) {
352						fillContent = grid[r - 1][c];
353					}
354
355					if (fillContent != null) {
356						grid[r][c] = fillContent;
357					} else if (grid[r][c] == null) {
358						grid[r][c] = "";
359					}
360				}
361			}
362		}
363
364		// 7. 将二维数组转换为 List<List<String>>
365		List<List<String>> normalizedTable = new ArrayList<>();
366		for (int r = 0; r < numRows; r++) {
367			List<String> normalizedRow = new ArrayList<>();
368			for (int c = 0; c < numCols; c++) {
369				normalizedRow.add(grid[r][c] == null ? "" : grid[r][c]);
370			}
371			normalizedTable.add(normalizedRow);
372		}
373
374		return normalizedTable;
375	}
376
377	/**
378	 * 将坐标四舍五入到指定精度,减少浮点误差
379	 */
380	private float roundCoordinate(float coord) {
381		return Math.round(coord * 10) / 10.0f;
382	}
383
384	/**
385	 * 查找单元格起始位置在网格中的索引
386	 */
387	private int findCellStartIndex(List<Float> coords, float value) {
388		float roundedValue = roundCoordinate(value);
389
390		for (int i = 0; i < coords.size(); i++) {
391			// 单元格的起始位置应该在某个网格线上或之前
392			if (roundedValue <= coords.get(i) + COORDINATE_TOLERANCE) {
393				return i;
394			}
395		}
396
397		return coords.size() - 1;
398	}
399
400	/**
401	 * 查找单元格结束位置在网格中的索引
402	 */
403	private int findCellEndIndex(List<Float> coords, float value) {
404		float roundedValue = roundCoordinate(value);
405
406		for (int i = coords.size() - 1; i >= 0; i--) {
407			// 单元格的结束位置应该在某个网格线上或之后
408			if (roundedValue >= coords.get(i) - COORDINATE_TOLERANCE) {
409				return i;
410			}
411		}
412
413		return 0;
414	}
415
416	/**
417	 * 将 Tabula 的 Table 对象转换为 List<List<String>>>
418	 * 
419	 * @param table Tabula 的 Table 对象
420	 * @return List<List<String>>>
421	 */
422	public List<List<String>> tableToListOfListOfStrings(Table table) {
423		// 创建一个列表来存储表格内容
424		List<List<String>> list = new ArrayList<>();
425
426		// 遍代表格中的每一行
427		for (List<RectangularTextContainer> row : table.getRows()) {
428			// 创建一个列表来存储当前行的内容
429			List<String> rowList = new ArrayList<>();
430
431			// 遍代当前行中的每一个单元格
432			for (RectangularTextContainer tc : row) {
433				// 将当前单元格的内容添加到行列表中
434				String cellText = tc.getText() == null ? "" : tc.getText().trim();
435				rowList.add(cellText);
436				rowList.add(tc.getText() == null ? "" : tc.getText().trim());
437			}
438
439			// 将行列表添加到表格列表中
440			list.add(rowList);
441		}
442		return list;
443	}
444
445	public static void main(String[] args) {
446		// 请替换为你的 PDF 文件路径
447		String pdfPath = "/Users/yuanzhenhui/Desktop/测试用合并单元格解析.pdf";
448
449		File pdfFile = new File(pdfPath);
450		if (!pdfFile.exists()) {
451			System.err.println("错误: 测试文件未找到: " + pdfPath);
452			System.err.println("请在 main 方法中替换为你本地的 PDF 文件路径。");
453			return;
454		}
455
456		MergedCellPdfExtractor extractor = new MergedCellPdfExtractor();
457		try {
458			System.out.println("开始解析: " + pdfPath);
459			List<List<List<String>>> tables = extractor.parseTables(pdfFile);
460
461			System.out.println("解析完成,共找到 " + tables.size() + " 个表格。");
462			System.out.println("========================================");
463
464			int tableNum = 1;
465			for (List<List<String>> table : tables) {
466				System.out.println("\n表格 " + (tableNum++) + ":");
467				System.out.println("行数: " + table.size() + ", 列数: " + (table.isEmpty() ? 0 : table.get(0).size()));
468				System.out.println("----------------------------------------");
469
470				for (List<String> row : table) {
471					System.out.print("|");
472					for (String cell : row) {
473						String cellText = cell.replace("\n", " ").replace("\r", " ");
474						if (cellText.length() > 15) {
475							cellText = cellText.substring(0, 12) + "...";
476						}
477						System.out.print(String.format(" %-15s |", cellText));
478					}
479					System.out.println();
480				}
481				System.out.println("----------------------------------------");
482			}
483
484		} catch (IOException e) {
485			System.err.println("解析 PDF 时出错: " + e.getMessage());
486			e.printStackTrace();
487		}
488	}
489}
490

关于代码的解释应该都清楚的了,由于只是用作试验我就没有很精细地封装了,大家凑合着用吧。如果面对更加复杂的表格的话我建议还是不要用这种填充的方式了,直接上大厂的 OCR 接口吧。

哦,还有东西忘了说了,关于 Maven 依赖的引入如下:

1<dependency>
2  <groupId>technology.tabula</groupId>
3  <artifactId>tabula</artifactId>
4  <version>1.0.5</version>
5</dependency>
6<dependency>
7  <groupId>org.apache.pdfbox</groupId>
8  <artifactId>pdfbox</artifactId>
9  <version>2.0.35</version>
10</dependency>
11

好了,该填的可能填好了。接下来的分享将继续回归到人工智能和区块链当中,欢迎您继续关注我的博客。


【Java】基于 Tabula 的 PDF 合并单元格内容提取》 是转载文章,点击查看原文


相关推荐


猿辅导Java面试真实经历与深度总结(二)
360_go_php2025/10/22

​ 在面试中,掌握Java的基础知识和深入的理解是非常重要的。今天,我们来解析几个常见的Java面试问题,包括线程状态、线程池、深拷贝与浅拷贝、线程安全、Lock与Synchronized的区别,以及逃逸分析等话题。 1. 线程状态 Java中,线程有七种状态,它们是由 Thread.State 枚举类定义的。线程的状态随着程序的执行而发生变化,下面是七种状态的描述:​编辑 NEW:线程被创建,但尚未启动。 RUNNABLE:线程可以运行,或者已经正在运行。线程调度器选择合适的线程让它执行


Docker 通信核心:docker.sock 完全指南
做运维的阿瑞2025/10/20

阅读时长: 15min | 难度: 中级 | 作者: 做运维的阿瑞 | 更新时间: 2025-10 文章目录 前言一、Docker 通信原理总览1.1 技术架构解析1.2 核心技术对比 二、核心用法与技巧2.1 容器内访问宿主机 Docker2.2 使用 Docker SDK2.3 直接与 API 交互 三、安全风险与最佳实践Q1: 有多危险?为什么说拿到 `docker.sock` 就等于 `root`?Q2: 如何安全地授权用户使用 Docker?Q3: 有没有比挂


自定义Spring Boot Starter项目并且在其他项目中通过pom引入使用
劝导小子2025/10/19

1、创建starter项目 我电脑只有JDK 8,但是创建项目的时候最低只能选择17,后续创建完后去修改即可 2、项目结构 删除主启动类Application:Starter不需要启动类删除配置文件application.properties:Starter不需要自己的配置文件删除test里面的测试启动类 在resources下创建META-INF文件夹 3、修改JDK 修改成JDK8,如果你有更高的版本请切换 4、配置pom.xml <?xml version="1


RabbitMQ消息传输中Protostuff序列化数据异常的深度解析与解决方案
Mr.45672025/10/18

目录 问题背景 环境配置 使用的依赖 测试对象 初始代码(有问题的版本) 问题分析 1. 初步排查 2. 关键发现 3. RabbitTemplate的默认行为分析 4. SimpleMessageConverter的处理机制 深入理解消息转换 消息转换器的层次结构: 而直接发送 Message: 解决方案 方案1:直接使用Message对象(推荐) 方案2:配置自定义MessageConverter 问题根因总结 经验教训 结论 最后最后附上序列化工具:


Apache Doris 与 ClickHouse:运维与开源闭源对比
SelectDB技术团队2025/10/16

引言 在当今数据驱动的商业环境中,OLAP(在线分析处理)数据库的选择对企业的数据分析能力和运维成本有着深远影响。Apache Doris 和 ClickHouse 作为业界领先的高性能 OLAP 数据库,各自在不同场景下展现出独特优势。 Apache Doris 以其优秀的宽表查询能力、多表 JOIN 性能、实时更新、search 以及湖加速特性而著称。ClickHouse 同样在宽表处理方面表现出色,其丰富的分析函数库和高性能单表聚合能力备受青睐。 然而,从运维角度来看,两者在存算分离


统一高效图像生成与编辑!百度&新加坡国立提出Query-Kontext,多项任务“反杀”专用模型
AI生成未来2025/10/15

论文链接:https://arxiv.org/pdf/2509.26641 亮点直击 Query-Kontext,一种经济型集成多模态模型(UMM),能够将视觉语言模型(VLMs)中的多模态生成推理与扩散模型执行的高保真视觉渲染相分离。 提出了一种三阶段渐进式训练策略,该策略逐步将 VLM 与越来越强大的扩散生成器对齐,同时增强它们在生成推理和视觉合成方面的各自优势。 提出了一种精心策划的数据集收集方案,以收集真实、合成和经过仔细筛选的开源数据集,涵盖多样的多模态参考到图像


微美全息(NASDAQ:WIMI)融合区块链+AI+IoT 三大技术,解锁物联网入侵检测新范式
爱看科技2025/10/14

在全面数字化转型的浪潮中,区块链、网络安全、人工智能与机器学习不再是孤立的技术概念,而是相互交织、共同推动行业进步的强大引擎。这些技术的紧密结合,特别是在物联网(IoT)领域的应用,正引领着一场前所未有的安全、效率与智能化变革。   实际,区块链技术以其去中心化、安全性和不可篡改性,为物联网数据存储和共享提供了全新的解决方案。而人工智能与机器学习技术的应用,使得物联网系统具备了自我学习和优化的能力。机器学习算法能够分析海量数据,识别出潜在的安全威胁或性能瓶颈,为系统提供精准的决策支持。


基于旗鱼算法优化卷积神经网络结合长短期记忆网络与注意力机制(CNN-LSTM-Attention)的风电场发电功率预测
智能算法研学社(Jack旭)2025/10/12

基于旗鱼算法优化卷积神经网络结合长短期记忆网络与注意力机制(CNN-LSTM-Attention)的风电场发电功率预测 文章目录 基于旗鱼算法优化卷积神经网络结合长短期记忆网络与注意力机制(CNN-LSTM-Attention)的风电场发电功率预测1.CNN原理2.LSTM原理3.注意力机制4.CNN-LSTM-Attention5.风电功率预测5.1 数据集6.基于旗鱼算法优化的CNN-LSTM-Attention7.实验结果8.Matlab代码 1.CNN原理 卷积神经


C++ const 用法全面总结与深度解析
oioihoii2025/10/10

1. const 基础概念 const 关键字用于定义不可修改的常量,是C++中确保数据只读性和程序安全性的核心机制。它可以应用于变量、指针、函数参数、返回值、成员函数等多种场景,深刻影响代码的正确性和性能。 1.1 本质与编译期处理 const变量在编译时会被编译器严格检查,任何修改尝试都会导致编译错误。与C语言不同,C++中的const变量(尤其是全局const)通常不会分配内存,而是直接嵌入到指令中(类似#define),但在以下情况会分配内存: 取const变量地址时 const变量为


php artisan db:seed执行的时候遇到报错
快支棱起来2025/10/9

INFO Seeding database. Illuminate\Database\QueryException SQLSTATE[42S22]: Column not found: 1054 Unknown column 'email_verified_at' in 'field list' (Connection: mysql, SQL: insert into users (name, email, email_verified_at, password, remember_token,

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0