PDF订单数据和尺码对不上。怎么办?python说好办

订单数据是可读PDF,嗨,这还不简单,直接读就完了。

此处应有截图。

读出来是这样的

Supplier SKU Description Color SKU Cost Ordered Amount Retail

85.00 9 765.00 160.00

5 5.5 6 6.5 7 7.5 8 8.5 9 9.5 10 10.5 11 12

1 1 1 1 1 1 1 1 1

85.00 8 680.00 160.00

5 5.5 6 6.5 7 7.5 8 8.5 9 9.5 10 10.5 11 12

1 1 1 1 1 1 1 1

5 5.5开始这一行是尺码,下一行是下单数量,这时候发现不对劲,未下单的尺码并没有空格隔开,直接读每次都是从最小码开始下单,这明显不符合实际情况。怎么办才可以反映出尺码和数量的匹配关系呢?

python里有一个库pdfplumber,号称是强大的pdf操作库,看看它能干什么

python 复制代码
import pdfplumber
import re
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side

def extract_pdf_with_layout(pdf_path, output_xlsx='订单详情.xlsx'):
    """
    使用pdfplumber提取PDF内容,输出到Excel文件
    每个商品一个sheet,第一行首列为SKU,第二行为尺码,第三行为数量
    """
    all_products = []
    
    with pdfplumber.open(pdf_path) as pdf:
        # 遍历所有页面
        for page_num, page in enumerate(pdf.pages, start=1):
            print(f'正在处理第 {page_num} 页...')
            
            # 提取文本(保留布局)
            text = page.extract_text(layout=True)
            
            if text:
                # 查找"Supplier SKU"的位置
                supplier_sku_index = text.find('Supplier SKU')
                
                if supplier_sku_index != -1:
                    # 只保留Supplier SKU之后的内容
                    relevant_text = text[supplier_sku_index:]
                    
                    # 解析商品信息
                    products = parse_products(relevant_text)
                    all_products.extend(products)
                    print(f'  找到 {len(products)} 个商品')
                else:
                    print(f'第 {page_num} 页未找到"Supplier SKU"')
    
    # 保存到Excel
    if all_products:
        save_to_excel(all_products, output_xlsx)
        print(f'\n数据已保存到: {output_xlsx},共 {len(all_products)} 个商品')
    else:
        print('未提取到任何数据')


def parse_products(text):
    """
    解析商品信息,返回商品列表
    """
    products = []
    lines = text.split('\n')
    
    # 找到表头行(Supplier SKU所在行)
    header_line_idx = None
    for i, line in enumerate(lines):
        if 'Supplier SKU' in line:
            header_line_idx = i
            break
    
    if header_line_idx is None:
        return products
    
    i = header_line_idx + 1
    processed_skus = set()
    
    while i < len(lines):
        line = lines[i].strip()
        
        # 跳过空行
        if not line:
            i += 1
            continue
        
        # 跳过明显的表头行或汇总行
        if 'PO Totals' in line or 'Page:' in line:
            i += 1
            continue
        
        # 检查是否是商品信息行
        # 修复:支持带逗号的Amount和Retail列
        # 格式:SKU ... Cost Ordered Amount Retail
        # Cost: \d+\.\d{2}
        # Ordered: \d+
        # Amount: [\d,]+\.[\d,]+ 或 \d+\.\d{2}
        # Retail: [\d,]+\.[\d,]+ 或 \d+\.\d{2}
        sku_match = re.match(r'^([A-Z0-9-]{5,})', line)
        if sku_match:
            # 匹配商品信息行的完整格式
            # 支持:Cost Ordered Amount Retail
            # 其中Amount和Retail可能带逗号
            price_patterns = [
                # 格式1: Cost(带逗号) Ordered Amount(带逗号) Retail(带逗号)
                r'[\d,]+\.[\d,]+?\s+\d+\s+[\d,]+\.[\d,]+?\s+[\d,]+\.[\d,]+',
                # 格式2: Cost(不带逗号) Ordered Amount(带逗号) Retail(带逗号)
                r'\d+\.\d{2}\s+\d+\s+[\d,]+\.[\d,]+?\s+[\d,]+\.[\d,]+',
                # 格式3: Cost(不带逗号) Ordered Amount(带逗号) Retail(不带逗号)
                r'\d+\.\d{2}\s+\d+\s+[\d,]+\.[\d,]+?\s+\d+\.\d{2}',
                # 格式4: Cost(不带逗号) Ordered Amount(不带逗号) Retail(带逗号)
                r'\d+\.\d{2}\s+\d+\s+\d+\.\d{2}\s+[\d,]+\.[\d,]+',
                # 格式5: Cost(不带逗号) Ordered Amount(不带逗号) Retail(不带逗号)
                r'\d+\.\d{2}\s+\d+\s+\d+\.\d{2}\s+\d+\.\d{2}',
                # 格式6: 至少包含一个价格格式(更宽松的匹配)
                r'[\d,]+\.[\d,]+',
            ]
            
            has_price_pattern = False
            for pattern in price_patterns:
                if re.search(pattern, line):
                    has_price_pattern = True
                    break
            
            if has_price_pattern:
                sku = sku_match.group(1)
                
                # 避免重复处理
                if sku in processed_skus:
                    i += 1
                    continue
                
                processed_skus.add(sku)
                print(f'\n  处理商品: {sku}')
                print(f'    商品行: {line[:100]}')
                
                # 查找尺码行和数量行
                size_line = None
                qty_line = None
                
                # 扩大搜索范围到15行,确保能找到
                for j in range(i + 1, min(i + 15, len(lines))):
                    next_line = lines[j]
                    next_line_stripped = next_line.strip()
                    
                    # 跳过空行
                    if not next_line_stripped:
                        continue
                    
                    # 如果遇到下一个商品行,停止搜索
                    if re.match(r'^[A-Z0-9-]{5,}', next_line_stripped):
                        # 检查是否真的是商品行(包含价格)
                        is_next_product = False
                        for pattern in price_patterns:
                            if re.search(pattern, next_line_stripped):
                                is_next_product = True
                                break
                        if is_next_product:
                            break
                    
                    # 查找尺码行(放宽条件)
                    if size_line is None:
                        # 检查是否包含小数尺码(如5.5, 6.5等)
                        if re.search(r'\b\d+\.5\b', next_line):
                            size_line = next_line
                            print(f'    找到尺码行: {next_line[:80]}')
                        # 或者检查是否包含多个连续的数字(至少8个数字,可能是尺码)
                        elif re.findall(r'\b\d+(?:\.5)?\b', next_line) and len(re.findall(r'\b\d+(?:\.5)?\b', next_line)) >= 8:
                            # 确保不是价格行(不包含XX.XX格式的价格,但允许整数)
                            if not re.search(r'\d+\.\d{2}', next_line):
                                size_line = next_line
                                print(f'    找到尺码行: {next_line[:80]}')
                    
                    # 查找数量行(放宽匹配条件)
                    if qty_line is None:
                        # 检查是否包含前缀格式(B/M, D/M, D/W, 2E/W, 2E/XW等)
                        if re.search(r'[A-Z0-9]+/[A-Z]+', next_line):
                            # 确保这一行主要是数字和空格(数量行特征)
                            numbers = re.findall(r'\b\d+\b', next_line)
                            if len(numbers) >= 3:  # 至少3个数字
                                qty_line = next_line
                                print(f'    找到数量行: {next_line[:80]}')
                
                # 解析尺码和数量(使用文本位置匹配)
                if size_line and qty_line:
                    sizes, quantities = extract_sizes_and_quantities_with_text_position(sku, size_line, qty_line)
                    
                    if sizes:
                        products.append({
                            'SKU': sku,
                            '尺码': sizes,
                            '数量': quantities
                        })
                        print(f'    尺码数量: {len(sizes)}, 订单数量: {sum(quantities)}')
                    else:
                        print(f'    警告: 解析失败,未提取到尺码')
                else:
                    print(f'    警告: 未找到尺码行或数量行')
                    if not size_line:
                        print(f'      未找到尺码行')
                    if not qty_line:
                        print(f'      未找到数量行')
                        # 输出接下来几行用于调试
                        print(f'      接下来5行内容:')
                        for k in range(i + 1, min(i + 6, len(lines))):
                            print(f'        {k}: {repr(lines[k][:80])}')
        
        i += 1
    
    return products


def extract_sizes_and_quantities_with_text_position(sku, size_line, quantity_line):
    """
    使用文本位置(字符索引)进行距离匹配
    修复:从数量出发找最近尺码,但不累加,每个尺码最多只有一个数量
    如果数量被替换,尝试匹配其他尺码,确保所有数量都被匹配
    """
    print(f'\n    === 开始处理 {sku} ===')
    print(f'    尺码行: {repr(size_line[:100])}')
    print(f'    数量行: {repr(quantity_line[:100])}')
    
    # 步骤1: 提取尺码及其位置
    size_pattern = r'\d+\.?\d*'
    sizes = []
    size_positions = []
    
    for match in re.finditer(size_pattern, size_line):
        size_str = match.group()
        # 验证是否是有效的尺码(排除价格等)
        if '.' in size_str:
            # 包含小数点,如果小数点后只有一位数字,可能是尺码
            if len(size_str.split('.')[1]) == 1:
                sizes.append(size_str)
                center_pos = (match.start() + match.end()) // 2
                size_positions.append(center_pos)
        else:
            # 整数,可能是尺码
            sizes.append(size_str)
            center_pos = (match.start() + match.end()) // 2
            size_positions.append(center_pos)
    
    print(f'    提取到尺码: {sizes}')
    print(f'    尺码位置: {size_positions}')
    
    # 步骤2: 找到B/M, D/M, D/W, 2E/W, 2E/XW等标记的位置(放宽匹配)
    prefix_match = re.search(r'[A-Z0-9]+/[A-Z]+', quantity_line)
    if prefix_match:
        prefix_end = prefix_match.end()
        search_text = quantity_line[prefix_end:]
        offset = prefix_end
        print(f'    找到前缀: {prefix_match.group()}, 结束位置: {prefix_end}')
    else:
        search_text = quantity_line
        offset = 0
        print(f'    未找到前缀,使用整行')
    
    # 步骤3: 提取订单数量及其位置
    quantity_pattern = r'\d+'
    quantities = []
    quantity_positions = []
    
    for match in re.finditer(quantity_pattern, search_text):
        qty = int(match.group())
        quantities.append(qty)
        center_pos = offset + (match.start() + match.end()) // 2
        quantity_positions.append(center_pos)
    
    print(f'    提取到数量: {quantities}')
    print(f'    数量位置: {quantity_positions}')
    print(f'    总数量: {sum(quantities)}')
    
    # 步骤4: 位置匹配算法(修复:确保所有数量都被匹配)
    # 初始化所有尺码的数量为0
    size_quantity_map = {size: 0 for size in sizes}
    # 记录每个尺码的匹配距离和匹配的数量索引
    size_distance_map = {size: float('inf') for size in sizes}
    size_qty_idx_map = {size: None for size in sizes}  # 记录匹配的数量索引
    
    # 从数量出发,每个数量找最近的尺码
    for qty_idx, (qty, qty_pos) in enumerate(zip(quantities, quantity_positions)):
        min_distance = float('inf')
        closest_size = None
        
        # 找到最近的尺码
        for size, size_pos in zip(sizes, size_positions):
            distance = abs(qty_pos - size_pos)
            if distance < min_distance:
                min_distance = distance
                closest_size = size
        
        # 如果找到匹配的尺码
        if closest_size:
            # 如果这个尺码还没有匹配,直接匹配
            if size_qty_idx_map[closest_size] is None:
                size_quantity_map[closest_size] = qty
                size_distance_map[closest_size] = min_distance
                size_qty_idx_map[closest_size] = qty_idx
                print(f'    匹配: 数量{qty_idx+1} {qty} (位置{qty_pos}) -> 尺码 {closest_size} (位置{size_positions[sizes.index(closest_size)]}, 距离{min_distance:.1f})')
            # 如果这个尺码已经有匹配,但当前距离更近,则替换
            elif min_distance < size_distance_map[closest_size]:
                # 被替换的数量需要重新匹配
                old_qty_idx = size_qty_idx_map[closest_size]
                old_qty = quantities[old_qty_idx]
                print(f'    替换: 数量{qty_idx+1} {qty} (距离{min_distance:.1f}) 替换 数量{old_qty_idx+1} {old_qty} (距离{size_distance_map[closest_size]:.1f}) -> 尺码 {closest_size}')
                
                # 更新匹配
                size_quantity_map[closest_size] = qty
                size_distance_map[closest_size] = min_distance
                size_qty_idx_map[closest_size] = qty_idx
                
                # 被替换的数量尝试匹配其他尺码(排除已匹配的尺码)
                matched_sizes = set(size for size, idx in size_qty_idx_map.items() if idx is not None)
                best_alt_size = None
                best_alt_distance = float('inf')
                
                for size, size_pos in zip(sizes, size_positions):
                    if size in matched_sizes:  # 跳过已匹配的尺码
                        continue
                    distance = abs(quantity_positions[old_qty_idx] - size_pos)
                    if distance < best_alt_distance:
                        best_alt_distance = distance
                        best_alt_size = size
                
                # 如果找到替代尺码,匹配它
                if best_alt_size and best_alt_distance <= 50:  # 距离阈值
                    size_quantity_map[best_alt_size] = old_qty
                    size_distance_map[best_alt_size] = best_alt_distance
                    size_qty_idx_map[best_alt_size] = old_qty_idx
                    print(f'      重新匹配: 数量{old_qty_idx+1} {old_qty} -> 尺码 {best_alt_size} (距离{best_alt_distance:.1f})')
                else:
                    print(f'      警告: 数量{old_qty_idx+1} {old_qty} 无法找到替代尺码')
            else:
                # 当前距离更远,尝试匹配其他尺码
                matched_sizes = set(size for size, idx in size_qty_idx_map.items() if idx is not None)
                best_alt_size = None
                best_alt_distance = float('inf')
                
                for size, size_pos in zip(sizes, size_positions):
                    if size in matched_sizes:  # 跳过已匹配的尺码
                        continue
                    distance = abs(qty_pos - size_pos)
                    if distance < best_alt_distance:
                        best_alt_distance = distance
                        best_alt_size = size
                
                # 如果找到替代尺码,匹配它
                if best_alt_size and best_alt_distance <= 50:  # 距离阈值
                    size_quantity_map[best_alt_size] = qty
                    size_distance_map[best_alt_size] = best_alt_distance
                    size_qty_idx_map[best_alt_size] = qty_idx
                    print(f'    替代匹配: 数量{qty_idx+1} {qty} -> 尺码 {best_alt_size} (距离{best_alt_distance:.1f}, 原目标{closest_size}已被占用)')
                else:
                    print(f'    警告: 数量{qty_idx+1} {qty} 无法找到匹配尺码')
    
    # 步骤5: 转换为列表格式,保持尺码顺序
    quantities_list = [size_quantity_map[size] for size in sizes]
    
    print(f'    最终结果: 尺码={sizes}')
    print(f'    最终结果: 数量={quantities_list}')
    print(f'    匹配到的数量总和: {sum(quantities_list)}')
    print(f'    原始数量总和: {sum(quantities)}')
    if sum(quantities_list) != sum(quantities):
        print(f'    ⚠️ 警告: 数量不匹配!丢失了 {sum(quantities) - sum(quantities_list)} 个数量')
    print(f'    === 处理完成 ===\n')
    
    return sizes, quantities_list


def save_to_excel(products, output_xlsx):
    """
    保存商品数据到Excel,每个商品一个sheet
    第一行首列为SKU,第二行为尺码,第三行为数量
    """
    wb = Workbook()
    wb.remove(wb.active)  # 删除默认sheet
    
    # 定义样式
    header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
    header_font = Font(color="FFFFFF", bold=True)
    data_fill = PatternFill(start_color="FFFFFF", end_color="FFFFFF", fill_type="solid")
    border = Border(
        left=Side(style='thin'),
        right=Side(style='thin'),
        top=Side(style='thin'),
        bottom=Side(style='thin')
    )
    center_alignment = Alignment(horizontal='center', vertical='center')
    
    for idx, product in enumerate(products, start=1):
        sku = product['SKU']
        sizes = product['尺码']
        quantities = product['数量']
        
        # 创建sheet,命名为sheet1, sheet2, sheet3...
        sheet_name = f"sheet{idx}"
        ws = wb.create_sheet(title=sheet_name)
        
        # 第一行:首列为SKU
        ws.cell(row=1, column=1, value='SKU')
        ws.cell(row=1, column=1).fill = header_fill
        ws.cell(row=1, column=1).font = header_font
        ws.cell(row=1, column=1).border = border
        ws.cell(row=1, column=1).alignment = center_alignment
        
        ws.cell(row=1, column=2, value=sku)
        ws.cell(row=1, column=2).fill = data_fill
        ws.cell(row=1, column=2).border = border
        ws.cell(row=1, column=2).alignment = center_alignment
        
        # 第二行:首列为"尺码",后面是尺码列表
        ws.cell(row=2, column=1, value='尺码')
        ws.cell(row=2, column=1).fill = header_fill
        ws.cell(row=2, column=1).font = header_font
        ws.cell(row=2, column=1).border = border
        ws.cell(row=2, column=1).alignment = center_alignment
        
        col = 2
        for size in sizes:
            cell = ws.cell(row=2, column=col, value=size)
            cell.fill = data_fill
            cell.border = border
            cell.alignment = center_alignment
            col += 1
        
        # 第三行:首列为"数量",后面是对应的数量(确保对齐)
        ws.cell(row=3, column=1, value='数量')
        ws.cell(row=3, column=1).fill = header_fill
        ws.cell(row=3, column=1).font = header_font
        ws.cell(row=3, column=1).border = border
        ws.cell(row=3, column=1).alignment = center_alignment
        
        col = 2
        # 确保数量和尺码对齐
        min_len = min(len(sizes), len(quantities))
        for i in range(min_len):
            cell = ws.cell(row=3, column=col, value=quantities[i])
            cell.fill = data_fill
            cell.border = border
            cell.alignment = center_alignment
            col += 1
        
        # 如果数量少于尺码,剩余位置填0
        if len(quantities) < len(sizes):
            for i in range(len(quantities), len(sizes)):
                cell = ws.cell(row=3, column=col, value=0)
                cell.fill = data_fill
                cell.border = border
                cell.alignment = center_alignment
                col += 1
        
        # 调整列宽
        ws.column_dimensions['A'].width = 15
        for col_idx in range(2, len(sizes) + 2):
            if col_idx <= 26:
                col_letter = chr(64 + col_idx)
            else:
                col_letter = chr(64 + (col_idx - 1) // 26) + chr(64 + (col_idx - 1) % 26 + 1)
            ws.column_dimensions[col_letter].width = 10
    
    wb.save(output_xlsx)


# 使用示例
if __name__ == "__main__":
    pdf_path = r"XXXXXX.pdf"
    output_xlsx = "订单详情.xlsx"
    
    extract_pdf_with_layout(pdf_path, output_xlsx)

    '''
核心功能模块
1. 主入口函数 extract_pdf_with_layout()
打开PDF并遍历所有页面
定位"Supplier SKU"表头,提取后续内容
调用解析函数提取商品数据
将结果保存到Excel
2. 商品解析函数 parse_products()
识别商品信息行:以SKU开头,包含价格格式(支持带逗号的金额)
查找尺码行:包含小数尺码(如5.5)或至少8个数字
查找数量行:包含前缀(B/M、D/M、D/W、2E/W等)且至少3个数字
去重:使用processed_skus避免重复处理
3. 尺码与数量匹配算法 extract_sizes_and_quantities_with_text_position()
3.1 数据提取阶段
尺码提取:使用正则\d+\.?\d*,过滤价格(小数点后两位),保留尺码(小数点后一位或整数)
数量提取:定位前缀(如D/M)后提取数字,记录每个数字的文本位置(字符索引)
3.2 位置匹配算法
核心思路:基于文本位置(字符索引)的距离匹配
算法流程:
初始化:所有尺码数量为0,记录匹配距离和数量索引
遍历数量:每个数量找最近的尺码
匹配规则:
尺码未匹配:直接匹配
尺码已匹配:比较距离,保留更近的匹配
被替换的数量:尝试匹配其他未使用的尺码
距离过远:尝试匹配其他尺码
距离阈值:50像素,超过则认为不匹配
3.3 算法特点
一对一匹配:每个尺码最多一个数量,每个数量只匹配一次
避免丢失:被替换的数量会重新匹配,确保所有数量都被使用
位置精确:使用文本位置中心点计算距离,而非简单顺序匹配
4. Excel导出函数 save_to_excel()
为每个商品创建独立工作表(sheet1, sheet2...)
格式:
第1行:SKU列 + SKU值
第2行:尺码列 + 所有尺码
第3行:数量列 + 对应数量(与尺码对齐)
样式:表头蓝色背景、白色加粗字体,数据行白色背景,带边框
技术要点
1. 正则表达式匹配
商品行识别:支持多种价格格式组合(带/不带逗号)
尺码提取:区分尺码(5.5)和价格(85.00)
数量行识别:支持多种前缀格式(B/M、D/M、2E/XW等)
2. 文本位置计算
使用字符索引而非像素坐标
计算匹配中心点:(start + end) / 2
距离计算:abs(数量位置 - 尺码位置)
3. 容错处理
搜索范围:扩大到15行,避免遗漏
多种匹配模式:支持不同格式组合
调试输出:输出关键步骤和中间变量,便于排查
数据流程
PDF文件  ↓提取文本(保留布局)  ↓定位"Supplier SKU"表头  ↓遍历商品信息行  ├─ 提取SKU  ├─ 查找尺码行(下一行或下几行)  └─ 查找数量行(下一行或下几行)  ↓文本位置匹配算法  ├─ 提取尺码及其位置  ├─ 提取数量及其位置  └─ 距离匹配(确保对齐)  ↓生成商品数据列表  ↓保存到Excel(每个商品一个sheet)
算法复杂度
时间复杂度:O(n × m),n为商品数量,m为尺码/数量数量
空间复杂度:O(n × m),存储尺码、数量及其位置信息
适用场景
半结构化PDF订单文件
包含商品SKU、尺码、数量的订单数据
需要将PDF数据转换为结构化Excel格式
代码优势
鲁棒性:支持多种格式变化(带逗号金额、不同前缀等)
精确性:基于位置的距离匹配,而非简单顺序
完整性:确保所有数量都被匹配,避免丢失
可维护性:模块化设计,调试输出完善
该代码实现了从半结构化PDF到结构化Excel的自动化转换,适用于批量处理订单文件。
'''

可以从pdf读出结构化的尺码和订单数据信息,直接打印出来是这样

找到尺码行: 5 5.5 6 6.5 7 7.5 8 8.5 9 9.5 10 10.5 11 12

找到数量行: 1 1 1 1 1 1 1 1 1

尺码行: ' 5 5.5 6 6.5 7 7.5 8 8.5 9 9.5 10 10.5 11 12 '

数量行: ' 1 1 1 1 1 1 1 1 1

前面的空格回来了。

再加入距离匹配算法,让程序不会因为5和5.5占用的字符不同导致匹配出错。

一对一匹配:每个尺码最多一个数量,每个数量只匹配一次

避免丢失:被替换的数量会重新匹配,确保所有数量都被使用

位置精确:使用文本位置中心点计算距离,而非简单顺序匹配

最后生成excel表格,把每个商品放在单独的sheet中,至此问题解决。

相关推荐
蓝净云2 小时前
如何从pdf中提取带层级的标题结构
python·pdf
DS随心转小程序2 小时前
ChatGPT和Gemini转pdf
人工智能·ai·chatgpt·pdf·豆包·deepseek·ds随心转
souyuanzhanvip1 天前
PDF24 工具箱 V11.23.0 免费离线 PDF 处理工具
pdf·实用工具
非凡ghost1 天前
批量校正图像方向(校正PDF页面方向)
windows·学习·pdf·软件需求
缘如风1 天前
Poppler一个PDF的c++库
pdf
喜欢吃豆1 天前
从「文件URL」到「模型可理解内容」:一套完整的文件上传与解析处理流程详解(含PDF/Excel/图片)
pdf·大模型·excel
夜喵YM1 天前
基于 Spire.XLS.Free for Java 实现无水印 Excel 转 PDF
java·pdf·excel
weixin_462446231 天前
使用 Docker / Docker Compose 部署 PdfDing —— 个人 PDF笔记
笔记·docker·pdf
苦逼的老王1 天前
《java-使用kkview+libreoffice 实现在线预览ppt、xls、doc、pdf..》
java·pdf·powerpoint