python批量提取离线html网页的字段信息

import os  # 导入操作系统模块,用于文件系统操作
import re  # 导入正则表达式模块,用于文本匹配
import pandas as pd  # 导入pandas库,用于数据处理和Excel输出
from bs4 import BeautifulSoup  # 导入BeautifulSoup库,用于HTML解析

# 定义主字段和嵌套字段结构
MAIN_FIELDS = [
    '主体名称', '行业门类', '行业信息',  # 定义企业基本信息字段
    '登记状态', '法定代表人', '经营场所', '实际经营地址', '经营范围'  # 定义企业登记信息字段
]

NESTED_FIELDS = {
    '网店信息': ['所属平台', '网店名称', '网店链接', '开店时间']  # 定义网店相关的嵌套字段
}


def format_nested_data(group, items):
    """将嵌套字段数据格式化为指定字符串格式"""
    if not items:  # 如果没有数据项,返回空字符串
        return ""
        
    if group == '网店信息':  # 如果是网店信息组
        # 只提取网店名称,并用顿号分隔
        store_names = [item.get('网店名称', '').strip() for item in items if item.get('网店名称', '').strip()]  # 从每项数据中提取网店名称并去空格
        # 去除重复的网店名称
        unique_store_names = list(dict.fromkeys(store_names))  # 使用字典的键唯一性去重,并保持原有顺序
        return "、".join(unique_store_names)  # 用顿号连接所有唯一的网店名称
    
    return ""  # 对于其他组,返回空字符串


def extract_field(soup, field):
    # 使用更精确的CSS选择器定位
    selector_map = {
        '主体名称': '.company-name, .enterprise-name, .ent-name, .enterprise-title, h1, .title',  # 定义主体名称可能的CSS选择器
    }
    
    if field in selector_map:  # 如果字段有对应的选择器
        elements = soup.select(selector_map[field])  # 使用选择器查找元素
        if elements:  # 如果找到元素
            for elem in elements:  # 遍历所有找到的元素
                text = elem.text.strip()  # 获取元素文本并去除首尾空格
                if text and ('主体名称' == field or not re.match(r'^\d+$', text)):  # 确保文本不为空且不是纯数字(除非是主体名称)
                    return text  # 返回找到的文本
    
    # 回退到原始文本搜索
    text_nodes = soup.find_all(text=re.compile(field))  # 查找包含字段名的文本节点
    for node in text_nodes:  # 遍历所有匹配的文本节点
        # 检查父元素的下一个元素
        next_elem = node.parent.find_next()  # 获取文本节点父元素的下一个元素
        if next_elem and next_elem.name not in ['script', 'style']:  # 确保下一个元素不是脚本或样式元素
            return next_elem.text.strip()  # 返回下一个元素的文本内容
        
        # 检查文本节点是否包含冒号,可能是"主体名称:XXX"的形式
        if ':' in node or ':' in node:  # 检查节点是否包含英文冒号或中文冒号
            text = node.strip()  # 获取节点文本并去除首尾空格
            if ':' in text:  # 如果包含英文冒号
                parts = text.split(':', 1)  # 以英文冒号分割文本(最多分割一次)
            else:  # 如果包含中文冒号
                parts = text.split(':', 1)  # 以中文冒号分割文本(最多分割一次)
            
            if len(parts) > 1 and field in parts[0]:  # 确保分割后有两部分,且第一部分包含字段名
                return parts[1].strip()  # 返回冒号后的值并去除首尾空格
    
    # 尝试查找可能包含字段的表格
    tables = soup.find_all('table')  # 查找所有表格
    for table in tables:  # 遍历所有表格
        rows = table.find_all('tr')  # 获取表格所有行
        for row in rows:  # 遍历每一行
            cells = row.find_all(['th', 'td'])  # 查找行中的所有表头和单元格
            if len(cells) >= 2:  # 确保至少有两个单元格(键值对)
                for idx in range(len(cells) - 1):  # 遍历除最后一个单元格外的所有单元格
                    if field in cells[idx].text:  # 如果单元格文本包含字段名
                        return cells[idx+1].text.strip()  # 返回下一个单元格的值
    
    return ""  # 如果未找到值,返回空字符串


def extract_nested_table_data(soup, group_name, subfields):
    """提取嵌套表格数据"""
    items = []  # 存储提取到的数据项
    debug_info = []  # 存储调试信息
    
    # 打印调试信息
    debug_info.append(f"正在查找: {group_name}")  # 记录当前正在查找的组名
    
    # 先尝试直接通过文本内容查找
    group_headers = soup.find_all(string=lambda text: text and group_name in text)  # 查找包含组名的文本节点
    
    if not group_headers:  # 如果没找到
        # 如果没找到,尝试使用正则表达式查找
        group_headers = soup.find_all(string=re.compile(group_name))  # 使用正则表达式查找
        
    debug_info.append(f"找到{len(group_headers)}个可能的标题元素")  # 记录找到的标题数量
    
    if not group_headers:  # 如果依然没有找到
        # 如果依然找不到,尝试查找可能包含此信息的div或表格
        if group_name == '网店信息':  # 特殊处理网店信息
            # 尝试查找可能包含网店信息的表格
            tables = soup.find_all('table')  # 查找所有表格
            for table in tables:  # 遍历所有表格
                rows = table.find_all('tr')  # 获取表格中的所有行
                if len(rows) > 1:  # 假设表格至少有一个标题行和一个数据行
                    cells = rows[1].find_all('td')  # 获取第二行(第一个数据行)的所有单元格
                    if len(cells) >= 4:  # 假设至少有4列对应我们需要的字段
                        # 检查表格内容是否看起来像网店信息
                        text_content = ' '.join([cell.text for cell in cells])  # 合并所有单元格文本
                        if any(keyword in text_content for keyword in ['平台', '网店', '链接']):  # 检查是否包含关键词
                            debug_info.append(f"通过关键词匹配找到可能的网店信息表格")  # 记录找到的表格
                            # 处理每一行数据
                            for row in rows[1:]:  # 跳过标题行,遍历数据行
                                cells = row.find_all('td')  # 获取行中的所有单元格
                                if len(cells) >= 4:  # 确保有足够的单元格
                                    item = {  # 创建数据项字典
                                        '所属平台': cells[0].text.strip(),  # 第一列为所属平台
                                        '网店名称': cells[1].text.strip(),  # 第二列为网店名称
                                        '网店链接': cells[2].text.strip() if len(cells) > 2 else '',  # 第三列为网店链接
                                        '开店时间': cells[3].text.strip() if len(cells) > 3 else ''  # 第四列为开店时间
                                    }
                                    items.append(item)  # 将数据项添加到结果列表
    
    # 处理找到的每个标题元素
    for header_idx, header in enumerate(group_headers):  # 遍历所有找到的标题元素
        debug_info.append(f"正在处理第{header_idx+1}个标题元素")  # 记录当前处理的标题索引
        
        # 查找表格 - 尝试多种方法
        tables = []  # 存储找到的表格
        
        # 方法1: 查找标题元素的下一个表格
        next_table = header.find_next('table')  # 获取标题元素后的第一个表格
        if next_table:  # 如果找到表格
            tables.append(next_table)  # 添加到表格列表
            debug_info.append("找到下一个表格")  # 记录日志
            
        # 方法2: 查找父元素中的表格
        parent = header.parent  # 获取标题元素的父元素
        for _ in range(5):  # 向上查找5层
            if parent:  # 如果父元素存在
                parent_tables = parent.find_all('table')  # 查找父元素中的所有表格
                if parent_tables:  # 如果找到表格
                    tables.extend(parent_tables)  # 将表格添加到列表
                    debug_info.append(f"在父元素中找到{len(parent_tables)}个表格")  # 记录日志
                parent = parent.parent  # 继续向上查找父元素
            else:
                break  # 如果没有父元素,退出循环
                
        # 方法3: 如果标题在表格标题中,直接找该表格
        current = header  # 从标题元素开始
        for _ in range(3):  # 向上查找3层
            if current.name == 'table':  # 如果当前元素是表格
                tables.append(current)  # 将表格添加到列表
                debug_info.append("标题本身在表格中")  # 记录日志
                break  # 退出循环
            elif current.parent:  # 如果有父元素
                current = current.parent  # 移动到父元素
            else:
                break  # 如果没有父元素,退出循环
                
        debug_info.append(f"总共找到{len(tables)}个相关表格")  # 记录找到的表格总数
                
        # 处理找到的每个表格
        for table in tables:  # 遍历所有找到的表格
            # 尝试多种选择器来找表格行
            selectors = [  # 定义可能的CSS选择器
                'tr.el-table__row',  # Element UI表格行
                'tr',  # 普通表格行
                '.el-table__row',  # Element UI表格行(不一定在tr中)
                'tbody tr'  # 表格体中的行
            ]
            
            for selector in selectors:  # 遍历所有选择器
                rows = table.select(selector)  # 使用选择器查找行
                debug_info.append(f"使用选择器'{selector}'找到{len(rows)}行")  # 记录找到的行数
                
                if rows:  # 如果找到行
                    # 处理每一行
                    for row_elem in rows:  # 遍历每一行
                        # 跳过标题行
                        if row_elem.find('th'):  # 如果行中包含表头单元格
                            continue  # 跳过这一行
                            
                        # 尝试多种方式获取单元格
                        cells = row_elem.select('td')  # 使用CSS选择器查找单元格
                        if not cells:  # 如果没有找到单元格
                            cells = row_elem.find_all('td')  # 使用find_all方法查找单元格
                            
                        if len(cells) >= len(subfields):  # 确保单元格数量不少于子字段数
                            item = {}  # 创建数据项字典
                            for field_idx, field in enumerate(subfields):  # 遍历子字段
                                # 尝试多种方式获取单元格内容
                                cell = cells[field_idx]  # 获取对应索引的单元格
                                # 先查找div,然后查找任何子元素,最后使用文本
                                content_elem = cell.select_one('div') or cell.find() or cell  # 获取内容元素
                                cell_content = content_elem.text.strip()  # 获取元素文本并去除首尾空格
                                item[field] = cell_content  # 将值存入数据项字典
                            items.append(item)  # 将数据项添加到结果列表
                    
                    # 如果找到了行,就跳出选择器循环
                    if items:  # 如果已经提取到数据项
                        break  # 退出选择器循环
            
            # 如果已经从这个表格中找到了项目,就跳出表格循环
            if items:  # 如果已经提取到数据项
                debug_info.append(f"成功从表格中提取了{len(items)}项数据")  # 记录提取成功
                break  # 退出表格循环
    
    print(f"\n===调试信息: {group_name}===")  # 打印调试信息标题
    for line in debug_info:  # 遍历所有调试信息
        print(line)  # 打印每行调试信息
    print(f"提取到{len(items)}项数据")  # 打印提取到的数据项数量
    if items:  # 如果有数据项
        print(f"第一项数据示例: {items[0]}")  # 打印第一个数据项作为示例
    print("==============\n")  # 打印分隔线
    
    return items  # 返回提取到的所有数据项


if __name__ == "__main__":  # 如果直接运行此脚本
    data = []  # 初始化数据列表
    html_files = [f for f in os.listdir('.') if f.endswith('.html')]  # 获取当前目录下所有HTML文件
    
    if not html_files:  # 如果没有找到HTML文件
        print("当前目录中没有找到HTML文件。请确保HTML文件与脚本在同一目录。")  # 打印提示信息
    else:  # 如果找到HTML文件
        print(f"找到{len(html_files)}个HTML文件: {', '.join(html_files)}")  # 打印找到的HTML文件数量和名称
    
    for file in html_files:  # 遍历每个HTML文件
        print(f"\n正在处理文件: {file}")  # 打印当前处理的文件名
        with open(file, 'r', encoding='utf-8', errors='ignore') as f:  # 打开HTML文件,忽略编码错误
            html_content = f.read()  # 读取文件内容
            soup = BeautifulSoup(html_content, 'html.parser')  # 创建BeautifulSoup对象解析HTML
            item_data = {}  # 初始化数据项字典
            
            # 提取主字段
            for field in MAIN_FIELDS:  # 遍历主字段
                value = extract_field(soup, field)  # 从HTML中提取字段值
                item_data[field] = value  # 将值存入数据项字典
                if field in ['主体名称']:  # 如果是主体名称字段
                    print(f"{field}: {value}")  # 打印主体名称的值
            
            # 提取嵌套字段
            for group, subfields in NESTED_FIELDS.items():  # 遍历嵌套字段组
                nested_items = extract_nested_table_data(soup, group, subfields)  # 提取嵌套表格数据
                formatted_data = format_nested_data(group, nested_items)  # 格式化嵌套数据
                item_data[group] = formatted_data  # 将格式化后的数据存入数据项字典
                print(f"{group} 提取结果: {formatted_data if formatted_data else '空'}")  # 打印提取结果
            
            data.append(item_data)  # 将数据项添加到数据列表
    
    # 验证数据是否有效
    if data:  # 如果有数据
        # 保留主字段和格式化后的嵌套字段
        columns = MAIN_FIELDS + list(NESTED_FIELDS.keys())  # 合并主字段和嵌套字段组名作为列
        df = pd.DataFrame(data, columns=columns)  # 创建DataFrame
        df.to_excel('output.xlsx', index=False)  # 导出为Excel文件
        print(f"\n成功提取{len(data)}个文件的数据,已保存到output.xlsx")  # 打印成功信息
    else:  # 如果没有数据
        print("\n未找到任何数据,请检查HTML结构或字段名称")  # 打印错误提示