查看原文
其他

数据治理 | 省下一个亿!一文读懂如何用python读取并处理PDF中的表格(赠送本文所用的PDF文件)

快点关注→ 数据Seminar 2023-02-21

目录

一、前言

二、背景案例

三、数据清洗流程

  1. 理解数据,明确要求

  2. 清洗数据流程

(1)PDF转化为EXCEL

(2)清洗&合并所有表

(3)按需求处理合并后的表

四、总结

本文共6039个字,阅读大约需要16分钟,欢迎指正!

如需获取文中所用到的PDF文件,可关注我们,对话框内发送关键词“20220708”~

Part1前言

对于社科研究者来说,我们经常会与各种统计数据、文档文献打交道,也会遇到许多不好编辑处理的数据,比如 PDF 文档。作为一种非结构化数据,PDF 是为阅读和稳定而生的,区别于 word 文档和 Excel 表这些可以随时修改的文档,PDF 的可编辑性非常差,但稳定性很高。也正因此,它常被用于保存一些社科领域内具有权威性的文字、表格等信息。
既然这样,那我们如何从 PDF 中获取表格信息呢?如果使用商业转换工具,可能会面临收费或页数,次数限制等,且不一定能够达到我们预期的效果。还不如我们自己动手处理,正所谓自己动手,丰衣足食,所以……“动手”教程这不就来了吗?且看本文教你如何拿捏 PDF 文档中的表格。
文中所有 Python 代码均在集成开发环境 Visual Studio Code (VScode) 中使用交互式开发环境 Jupyter Notebook 编写和运行。

Part2案例背景

有 9 个 PDF 文档需要处理,每个文档中都保存着一些表格,其中一个 PDF 的部分内容截图如下:
这些表格中保存着一些学术数据库的表名称和字段名,现在需要获取这些 PDF 中的所有表格,把这些表格合并为一张表,处理成以下样式(内容对应上面的截图)并保存为 Excel 表

也就是说,需要完成以下操作:
  1. 从 PDF 中读取表格,并将所有表格合并为一张表
  2. 表名称字段分割为表格名称_英文表格名称_中文两个字段
  3. 将一个表名下的所有中英文变量名分别合并在一起,使用顿号“、”连接在一起

Part3数据清理流程

1理解数据,明确需求

Python 中可以读取 PDF 的第三方库有不少,但基本上都只能读取内容为非图片格式的 PDF,如果 PDF 中存有图片,则必须依靠光学字符识别技术(OCR)才能获取图片中的数据信息。而我们要处理的 PDF 并不是使用图片来保存数据的,所以我们使用 Python 第三方库 pdfplumber 即可读取所需表,使用第三方库 xlwt 可以将读取到的表格写入 Excel 中。
另外,观察数据发现,部分PDF中存有不止一个表格,合并表格时需要注意删除多余的表头。而数据处理需求也已经在上文中阐述清楚。

2数据清洗流程

(1) PDF 转化为 Excel

一共 9 个文件需要处理,如果每处理一份 PDF 都需要手动修改文件路径,显然是效率低下的。所以我们可以使用 Python 标准库 os 一次性获取所有 PDF 的文件路径。我们将所有待处理 PDF 全部放入 Python 工作目录下的一个名为转换前的PDF文件夹中,使用下面的方法就可以获取该文件夹下所有 PDF 的路径:
# 获取某文件夹下所有指定后缀名文件的路径
import os
def get_file_list(path, ex):
    """
    获取指定路径下的符合后缀名条件的文件路径
    path: 监测目录的路径
    ex: 条件,文件后缀名
    return: 指定后缀名文件路径列表
    "
""
    file_path_list = []
    for dir, folder, file in os.walk(path):
        for i in file:
            if os.path.splitext(i)[1] in ex:
                file_path_list.append(os.path.join(dir, i))
    return file_path_list

# 获取路径,文件所在目录(文件全部存放在一个文件夹下,最好不要设置子文件夹,不要存放其他文件)
# 获取 文件夹 PDF下所有后缀名是 .pdf 的文件路径
pdf_path_list = get_file_list('./转换前的PDF', ['.pdf'])
pdf_path_list

输出结果如下图所示:

我们先将每一个 PDF 中的表格,转换为 Excel 表,再合并处理所有 Excel 表。
使用 Python 第三方库 pdfplumberxlwt 定义一个可以将一个 PDF 中所有表格信息转换为 Excel 表的方法,并将所有转换后的 Excel 表存放在名为 转换后的Excel 的文件夹下,文件名称不变。代码如下:
import pdfplumber
import xlwt

def pdf_to_excel(pdf_path, excel_path):
    '''
    参数说明:
    pdf_path:   pdf 文档的路径
    excel_path: 保存 excel 表的路径
    '
''
    workbook = xlwt.Workbook()            # 定义一个空的工作表,名为workbook
    sheet = workbook.add_sheet('Sheet1')  # 为工作表添加一个 sheet,sheet名称为 'Sheet1',变量名为 sheet

    # 使用 pdfplumber.open 方法读取pdf 中所有信息
    pdf = pdfplumber.open(pdf_path)

    i = 0   # Excel 行序号起始位置,Python 中往往从 0 开始是计算

    # 以 PDF 的一页为单位,循环读取所有 PDF 页
    for page in pdf.pages:
        # page.extract_text() 表示获取 PDF 每一页的所有文字信息,包括表格、页眉页脚页码。 
        # 而 page.extract_tables() 则表示获取 PDF 每一页的所有表格,不含页眉页脚页码,
        # 根据我们的需求,使用 page.extract_tables() 最合适。
        for table in page.extract_tables():
            # table 表示这一页的所有表格,格式为多维列表
            for row in table:
                # row 表示表 table 中的一行,格式为列表
                for j in range(len(row)):
                    # j 表示一行数据 row 中数据单元格的序号,从 0 开始计算
                    # 将一行数据 row 的第 j 个数据(row[j]) 写入sheet 的第 i 行,j 列
                    sheet.write(i, j, row[j])
                # 一次写入一行数据,写操作完成后行序号 i 就会加1,表示稍后在下一行写入数据
                # 以这样的方式将 pdf 表中的每一个单元格,写入 Excel 中对应的单元格
                i += 1
    # 关闭读取的 pdf 
    pdf.close()
    # 将写好的 Excel 表保存到指定位置
    workbook.save(excel_path)
    # 输出一个转换成功的信号
    print('写入excel成功!')


# 调用上面的方法转换 PDF 为 Excel 表
for pdf_path in pdf_path_list:
    # 从 pdf 路径中提取文件名称,并生成对应的 excel 保存路径
    excel_name = pdf_path.split('\\')[-1].replace('.pdf''.xlsx')
    excel_path = os.path.join('./转换后的Excel', excel_name)
    # 开始转换
    pdf_to_excel(pdf_path, excel_path)
执行以上代码后,所有转换后的表被保存到指定文件夹中:

(2) 清洗&合并所有表

打开查看其中几张表,发现表名称字段有一些表名被存在两个单元格中:
所以我们合并前需要将表名称字段中的分布在不同单元格的表名称写入一个单元格中,合并表名称。清洗以及合并所有表的代码如下:
# 使用前面定义的获取指定后缀名文件路径的方法,来获取转换好的 Excel 路径
excel_path_list = get_file_list('./转换后的Excel', ['.xlsx'])

# 建立一个空的表,随后将所有处理后的表都并入这张表
DATA = pd.DataFrame()

# 遍历所有 Excel 表路径
for path in excel_path_list:

    # 读取一张表
    data = pd.read_excel(path)
    # 将这张表中重复出现的表头删除
    # 将‘表名称’字段设置为数据索引后,删除索引为‘表名称’的行,原来的表头不受影响
    try:
        data = data.set_index('表名称').drop('表名称').reset_index()
        data.reset_index(drop=True, inplace=True)   # 重置数据索引,使索引恢复连续状态
    except:
        pass

    # 合并位于不同单元格的表名称,主要思路是:
    # 若出现连续两行数据不为空,且后面一行中不含汉字,则将下面一行的字符加入到上面那一行中
    # 检测是否为空使用 pd.isna() 方法,检测是否含有汉字使用正则表达式
    for i in data.index:
        if pd.isna(data['表名称'][i]) == False and \
           pd.isna(data['表名称'][i+1]) == False and\
           bool(re.search('[\u4e00-\u9fa5]', data['表名称'][i+1])) == False:
            # 修改数据
            data['表名称'][i] += data['表名称'][i+1]
            data['表名称'][i+1] = np.nan

    # 清洗表名称字段,去除其中的换行符
    data['表名称'] = data['表名称'].fillna('')
    data['表名称'] = data['表名称'].apply(lambda x: x.replace('\n'''))
    
    # 两处理后的单张表添加到大数据表 DATA 中
    DATA = pd.concat([DATA, data])

print(DATA.shape)
DATA
运行上面的代码后,所有的表都被存入这一张表中,数据总行数为 3692:

(3) 按需求处理合并后的表

我们不妨先处理第三个需求,即分别合并一个表名下的所有中英文字段名。
Python 中进行这种操作的方法很多,我们可以自己一步步编写代码来实现,也可以使用 Pandas 中已经定义好的方法。但是后者效率要更加高,且更加准确。下面我们使用 Pandas 中的分组功能实现合并字段名:
# 先将空字符替换为空值,下面使用其他填充方式重新填充
DATA['表名称'] = DATA['表名称'].replace('', np.nan)
# 向下填充, 一列中的空值使用上一个非空值来填充
DATA['表名称'] = DATA['表名称'].ffill()
# 防止报错,填充其他字段中的缺失值
DATA['字段名称_英文'] = DATA['字段名称_英文'].fillna('无')
DATA['字段名称_中文'] = DATA['字段名称_中文'].fillna('无')

# 分组合并的自定义方法
def concat_function(x):
    return pd.Series(
        {
            '字段名称_英文':'、'.join(x['字段名称_英文']),
            '字段名称_中文':'、'.join(x['字段名称_中文'])
        }
    )
# 关键步骤,先根据'表名称'进行分组,分组后使用'、'将一组下面的所有元素拼接在一起,最后重置索引
DATA = DATA.groupby(DATA['表名称']).apply(concat_function).reset_index()
DATA
合并效果如下图所示:

接下来只需要将表名称字段拆分为表名称_英文表名称_中文字段即可,我们注意到,所有的表名称格式都是一样的,即中文名称在前,英文名称在最后,放在括号里面。我们正好可以使用正则表达式来处理。将表名称字段拆分为两个字段的代码如下:
# 标准化括号,将所有英文括号替换为中文括号,防止出错
DATA['表名称'] = DATA['表名称'].apply(lambda x: x.replace('(''(').replace(')'')'))
# 定义拆分方法
def split_function(df):
    Table_name = df['表名称']
    # 从完整的表明中提取中英文表名称
    # 提取英文名,使用正则表达式获取所有括号内的内容,取最后一个即可
    name_eng = re.findall('(.*?)', Table_name)[-1]
    name_eng = name_eng.strip('(').strip(')')
    # 提取中文名,从原来的表名称中删除英文名即可
    name_cn = Table_name.replace(f'({name_eng})''')
    # 返回提取结果
    return name_eng, name_cn

# 事先在指定位置插入两列
DATA.insert(1, '表名称_中文''')
DATA.insert(1, '表名称_英文''')
DATA[['表名称_英文''表名称_中文']] = DATA.apply(split_function, axis=1, result_type='expand')

# 删除原来的表名称
DATA.drop(['表名称'], axis=1, inplace=True)
DATA
拆分后的效果如下图所示:
最后将处理后的表保存为本地 Excel 文件就可以了:
DATA.to_excel('变量清单_合并.xlsx', index=False)

Part4总结

PDF 表格转 Excel 表是一项非常实用的技术,本期分享的数据清洗案例不仅有表格的转换,还包含了分组合并,一列变多列等 Pandas 表格处理技巧,这充分说明 Pandas 在数据处理中有着举足轻重的地位。
另外,如需获取文中所用到的PDF文件,可关注我们,对话框内发送关键词“20220708”~用于帮助大家测试代码以及了解我们的数据产品,希望对大家有所帮助。
我们将在数据治理板块中推出一系列原创推文,帮助读者搭建一个完整的社科研究数据治理软硬件体系。该板块将涉及以下几个模块(点击标题即可跳转至相应合集):
  1. 计算机基础知识
  2. 编程基础
  3. 数据采集
  4. 数据存储
  5. 数据清洗
  6. 数据实验室搭建
  7. 数据治理特别篇




星标⭐我们不迷路!想要文章及时到,文末“在看”少不了!

点击搜索你感兴趣的内容吧

往期推荐


基本无害 | 第三章第一节(全)—— 回归的基本原理

基本无害 | 使回归有意义——回归和因果关系(2)

基本无害 | 使回归有意义——回归和因果关系(1)

数据治理 | 教你三招,提升你的电脑安全系数!

数据治理 | 从“今天中午吃什么”中学习Python文本相似度计算

数据治理 | 地址数据可视化—教你如何绘制地理散点图和热力图

数据治理 | 根据地址获取经纬度及行政区划——API的妙用





数据Seminar




这里是大数据、分析技术与学术研究的三叉路口


文 | 《社科领域大数据治理实务手册》


    欢迎扫描👇二维码添加关注    

点击下方“阅读全文”了解更多

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存