两种方法去除页眉页脚
前言
如何去掉PDF或者WORD的页眉或者页脚?
由于需求涉及文本比对,页眉页脚会影响比对准确率,当前试过两种可以有效去除页眉页脚的方法,供大家参考思路和方法。
1.基于转换为图片后的页眉页脚高度定位识别切割
核心思路:首先将文档每一页转为图片,基于opencv的方法将图片二值化(即能分开空白区域和黑色文字像素),将图片降维存储为“心电图”,即可理解为将图片从上往下横向扫描切割,出现黑丝文字的区域越密集,“心电图”波峰越大。之后即可找到第一次出现波动的高度和该波动第一次消失的高度,第一次波动和最后一次波动即可分别视为页眉页脚出现的位置。(基于认知:页眉页脚应该出现在文档的最高处和最底处)
步骤:
1.首先将文档转为图片,网上有很多方法,可自行搜索。
2.比如得到测试用例:
3.对图片进行处理,进行横向映射:
split_img.py:
# coding:utf-8
import cv2
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
class SplitImg():
def __init__(self):
self.width = 0
self.height = 0
self.img_position = []
self.img_matrix = []
def dimension_reduction(self, img_path):
'''
对图片进行垂直和水平映射
:param img_path: 图片存储路径
:return:
'''
img = cv2.imread(img_path, 0)
_, self.img_matrix = cv2.threshold(img, 200, 255, cv2.THRESH_BINARY)
print(self.img_matrix)
horizontal_sum = np.sum(self.img_matrix, axis=1)
plt.plot(horizontal_sum,range(horizontal_sum.shape[0]))
plt.gca().invert_yaxis()
plt.show()
bin = self.img_matrix.T
vertical_sum = np.sum(bin, axis=1)
# plt.plot(range(vertical_sum.shape[0]),vertical_sum)
# plt.gca().invert_yaxis()
# plt.show()
self.height = horizontal_sum.shape[0]
self.width = vertical_sum.shape[0]
self.img_position = [[0, 0], [self.width, 0], [self.width, self.height], [0, self.height]]
def extra_space(self, dirc, apex_position, img_matrix, width_space):
if dirc == '-':
mapping_sum = np.sum(img_matrix, axis=1)
else:
mapping_sum = np.sum(img_matrix.T, axis=1)
mapping_size = mapping_sum.shape[0]
mapping_space = []
split_result = {}
start_index = 0
pre_num = mapping_sum[0]
stat_num = 0
for i in range(1, mapping_size):
if pre_num+1000 > mapping_sum[i] > pre_num-1000:
stat_num += 1
else:
if stat_num > self.width * width_space and start_index != 0:
# if stat_num > self.width * 0.1 and start_index != 0:
mapping_space.append((start_index+i-1)//2)
stat_num = 0
start_index = i
pre_num = mapping_sum[i]
if dirc == '-':
x0_y1 = apex_position[0]
x1_y1 = apex_position[1]
for i in range(len(mapping_space)):
x0_y0 = x0_y1
x1_y0 = x1_y1
x1_y1 = [apex_position[1][0], apex_position[1][1]+mapping_space[i]]
x0_y1 = [apex_position[0][0], apex_position[0][1]+mapping_space[i]]
split_result[str(i+1)] = [x0_y0, x1_y0, x1_y1, x0_y1]
split_result[str(i+2)] = [x0_y1, x1_y1, apex_position[2], apex_position[3]]
else:
x1_y0 = apex_position[0]
x1_y1 = apex_position[3]
for i in range(len(mapping_space)):
x0_y0 = x1_y0
x0_y1 = x1_y1
x1_y0 = [apex_position[0][0]+mapping_space[i], apex_position[1][1]]
x1_y1 = [apex_position[0][0]+mapping_space[i], apex_position[2][1]]
split_result[str(i+1)] = [x0_y0, x1_y0, x1_y1, x0_y1]
split_result[str(i+2)] = [x1_y0, apex_position[1], apex_position[2], x1_y1]
return split_result
def split_img(self, width_space): #返回按空白行切割的模块
dirc = '-'
hori_blocks = self.extra_space(dirc, self.img_position, self.img_matrix, width_space)
return hori_blocks
def split_img_cp(self, width_space):
dirc = '-'
hori_blocks = self.extra_space(dirc, self.img_position, self.img_matrix, width_space)
print(hori_blocks)
dirc = '|'
split_result = {}
for k,v in hori_blocks.items():
current_matrix = np.asarray(self.img_matrix)[v[0][1]:v[2][1],v[0][0]:v[1][0]]
vert_blocks = self.extra_space(dirc, v, current_matrix, width_space)
for k1,v1 in vert_blocks.items():
split_result['.'.join([k,k1])] = v1
return split_result
def run(self, img_path, width_space=0.01):
self.dimension_reduction(img_path)
return self.split_img(width_space)
3.去掉图片中的第一簇和最后一簇,得到结果:
这里没有去掉页脚的原因,代码中添加了误删逻辑,如果页眉页脚高度超过了整个文档比例的14%,则不删掉。
remove_img_header_footer.py:
from split_img import *
from file_diff.log import logger
import os
def deleteDir(path):
"""
删除路径或者文件
:return:
"""
try:
if os.path.isdir(path):
shutil.rmtree(path)
if os.path.isfile(path):
os.remove(path)
except Exception as e:
error = traceback.format_exc().split('\n')
logger.error('错误:\n{}'.format('\n'.join(error)))
def run_remove_header_and_footer(img_path_dir,width_space=0.01):
file_list = []
for root, dirs, files in os.walk(img_path_dir):
file_list = [(os.path.join(root, file), width_space) for file in files]
for img_path,ws in file_list:
remove_header_and_footer(img_path,ws)
# print(file_list)
def judge_exist_header(img_path_dir,width_space=0.01):
file_list = []
for root, dirs, files in os.walk(img_path_dir):
file_list = [(os.path.join(root, file), width_space) for file in files]
if(len(file_list)==1): #文件转为图片后只有一张
#利用word允许的页眉最大高度判断,如果在高度以内存在文字,则默认为存在页眉
#自己创建一个word最大高度的页眉页脚,填充上文字,然后转成PDF转成图片作为测试用例
#经过验证后不可行,页眉可以设置成无穷高度
#所以当只有一页图片时,为了稳妥起见,不处理
pass
elif(len(file_list)>1):
header_coordinates = []
footer_coordinates = []
for img_path, ws in file_list:
logger.info("start to judge_exist_header")
si = SplitImg()
result = si.run(img_path, width_space)
# 打开一张图
img = Image.open(img_path)
# 图片尺寸
img_size = img.size
print(img_size)
w = img_size[0] # 图片宽度
h = img_size[1] # 图片高度
for i, row_block in enumerate(result.values()):
if (i == 0):
logger.info('收集页眉坐标')
header_coordinates.append((row_block[3],row_block[2])) #页眉矩形模块的下端坐标,从左向右
elif (i == len(result.values()) - 1):
logger.info('收集页脚坐标')
footer_coordinates.append((row_block[0], row_block[1])) #页脚矩形模块的上端坐标,从左向右
print("页眉坐标:",header_coordinates)
print("页脚坐标:",footer_coordinates)
def judge_exist_footer():
pass
def remove_header_and_footer(img_path, width_space=0.01):
logger.info("start to remove header and footer")
try:
si = SplitImg()
result = si.run(img_path, width_space)
img = Image.open(img_path)
img_size = img.size
print(img_size)
w = img_size[0] # 图片宽度
h = img_size[1] # 图片高度
main_body = []
print("len(result.values()):",len(result.values()))
for i, row_block in enumerate(result.values()):
if (i == 0):
logger.info('跳过页眉')
if(row_block[3][1]/h>=0.14): #如果页眉高度超过14%,则不切以防误伤到正文
print("检测高度超过14%,不切页眉")
main_body.append(row_block[0])
main_body.append(row_block[1])
else:
main_body.append(row_block[3])
main_body.append(row_block[2])
elif (i == len(result.values()) - 1):
logger.info('跳过页脚')
if ((h-row_block[0][1]) / h >= 0.14): # 如果页脚高度超过14%,则不切以防误伤到正文
print("检测高度超过14%,不切页脚")
main_body.append(row_block[2])
main_body.append(row_block[3])
else:
main_body.append(row_block[1])
main_body.append(row_block[0])
# print(row_block)
print(main_body)
x = main_body[0][0]
y = main_body[0][1]
w = main_body[1][0] - main_body[0][0]
h = main_body[2][1] - main_body[1][1]
print(x,y,w,h)
if(w==0 or h==0):
# img.save(img_path)
deleteDir(img_path)
else:
region = img.crop((x, y, x + w, y + h))
# region.save(img_path[:-4]+"_removed_hdr_ftr"+img_path[-4:])
region.save(img_path)
except Exception as e:
logger.error(e)
return
logger.info("success remove header and footer")
if __name__ == '__main__':
remove_header_and_footer('img.png',width_space=0.01)
其中width_space=0.01是阈值,该值越小则越苛刻(切的厉害,稍微一点白缝可能就是边界),越大越包容(不切)。页面高度0.14也可以根据文档的情况自行设置调整。
另外log.py和本案例关系不大主要是调试用,大家自行删除或者替换为print()即可。
之后再通过OCR图片识别的方法自然就没有页眉页脚了,可以发现,该方法可能存在误切到正文的问题,并且可能需要根据文档页眉页脚本身的特点调整参数。
2.基于OCR识别后的文本去除
思路:首先将文档直接转为图片,图片再通过OCR识别转为文字,传入Dict格式的预处理结果,具体格式为:
{‘jpg name1’: ‘content1’, ‘jpg name2’: ‘content2’, …}
测试用例如下:
dict1 = {
‘001.jpg’: ‘这里是页眉\nahhfadvdajv\n\n1’,
‘002.jpg’: ‘这里是页眉\n啊甘好难过好\n但是不管你会给你\n2’,
‘003.jpg’: ‘这里是页眉\n北京公司\n技术同\n甲方:(委托人)\n乙方:(受托人)\n根据《中华人民共和国合同法》的规定,XXXX\n3’,
‘004.jpg’: ‘这里是页眉\n北京有限公司\nsdivndjsdvnjsd\n\n4’
}
然后按照以下步骤处理:
1.首先通过“\n”换行符切割每一张图片的文本
2.根据切割后的结果得到每一页的list,那么页眉可能在每一页的list[0]出现,页脚可能在每一页的list[-1]出现,基于这样的思想,再利用文本相似度+基于最高频出现的文本长度的容错投票筛选机制来判断
这里主要是利用了所有页面的全局信息,以判断页眉为例子,即比较所有页面的list[0],list[1]…,判断他们的相似性,如果相似度超过0.8(可调整)即认为页眉内容相同,但有时候页眉页脚就是单纯的数字(如第一页“1”,第二页“2”),这样相似度就不管用了,引入第二种判断机制:基于认知:每一页页眉/页脚的文本格式长度应该相同,那么对于一个文档而言,通过OCR识别之后的页眉页脚长度也应该相同或者相似,那么当大部分页面比如50%以上的页面(投票思想,阈值可设置)的list[0]的长度都为X时,即可能list[0]都是页眉(判断list[-1]都是页脚的思想同理)。
3.按照上述思想依次比较每一页的相同行list[idx],直到该行在每一页的相似度和长度都不满足阈值时即可退出(对应代码中的:if (avg_similar_score <= 0.5 and vote_ratio <= 0.5): break
)
remove_str_header_footer.py:
import difflib
from collections import Counter,defaultdict
def string_similar(s1, s2):
return difflib.SequenceMatcher(None, s1, s2).quick_ratio()
def process_header(dict,page_to_remove):
print("识别页眉")
avg_similar_score = 1
vote_ratio = 1
idx = 0
while (avg_similar_score > 0.5 or vote_ratio > 0.5):
header_list = []
header_content_len = []
for item in dict.items():
str = item[1].split("\n")
# print(str)
header_list.append({"str": str[idx], "len": len(str[idx])})
header_content_len.append(len(str[idx]))
times = 0
total_score = 0
for i in range(0, len(header_list)):
for j in range(i + 1, len(header_list)):
times += 1
score = string_similar(header_list[i]["str"], header_list[j]["str"])
total_score += score
avg_similar_score = total_score / times
# 计算字符串相同长度出现最多次数的频率占比
dic = Counter(header_content_len)
dic = sorted(dic.items(), key=lambda item: item[1], reverse=True)
vote_ratio = dic[0][1] / len(header_content_len)
if (avg_similar_score <= 0.5 and vote_ratio <= 0.5):
break
print(header_list)
print("avg_similar_score:", avg_similar_score)
print("出现最多的相同长度为 {} 的字符串次数:{},在所有页面中占比:{}".format(dic[0][0], dic[0][1], vote_ratio))
# 如果平均相似度>0.8,直接全部删除
if (avg_similar_score >= 0.8):
for i,item in enumerate(dict.items()):#遍历每一页
# print(item[1])
str = item[1].split("\n") #每一页按行分割
page_to_remove[i].append(idx) #第i页添加需要去掉的list索引
# 如果相似度太低,按照最多相同长度为中心的[-2,2]范围,符合这个范围的页眉页脚都删掉
else:
for i,item in enumerate(header_list):#遍历每一页
if(item["len"]>=dic[0][0]-2 and item["len"]<=dic[0][0]+2):
page_to_remove[i].append(idx)
idx += 1
def process_footer(dict,page_to_remove):
print("\n识别页脚")
avg_similar_score = 1
vote_ratio = 1
idx = -1
while (avg_similar_score > 0.5 or vote_ratio > 0.5):
header_list = []
header_content_len = []
for item in dict.items():
# print(item[0],item[1])
str = item[1].split("\n")
# print(str)
header_list.append({"str": str[idx], "len": len(str[idx])})
header_content_len.append(len(str[idx]))
times = 0
total_score = 0
for i in range(0, len(header_list)):
for j in range(i + 1, len(header_list)):
times += 1
score = string_similar(header_list[i]["str"], header_list[j]["str"])
total_score += score
avg_similar_score = total_score / times
# 计算字符串相同长度出现最多次数的频率占比
dic = Counter(header_content_len)
dic = sorted(dic.items(), key=lambda item: item[1], reverse=True)
vote_ratio = dic[0][1] / len(header_content_len)
if (avg_similar_score <= 0.5 and vote_ratio <= 0.5):
break
print(header_list)
print("avg_similar_score:", avg_similar_score)
print("出现最多的相同长度为 {} 的字符串次数:{},在所有页面中占比:{}".format(dic[0][0], dic[0][1], vote_ratio))
# 如果平均相似度>0.8,直接全部删除
if (avg_similar_score >= 0.8):
for i, item in enumerate(dict.items()): # 遍历每一页
page_to_remove[i].append(idx) # 第i页添加需要去掉的list索引
# 如果相似度太低,按照最多相同长度为中心的[-2,2]范围,符合这个范围的页眉页脚都删掉
else:
for i, item in enumerate(header_list): # 遍历每一页
if (item["len"] >= dic[0][0] - 2 and item["len"] <= dic[0][0] + 2):
page_to_remove[i].append(idx)
idx -= 1
def remove_run(dict):
page_to_remove = defaultdict(list)
process_header(dict, page_to_remove)
process_footer(dict, page_to_remove)
print("\n<key:每一页,value: 该页需要删除的段落索引list>:\n",page_to_remove)
result = ""
for i, item in enumerate(dict.items()): # 遍历每一页
str = item[1].split("\n") # 每一页按行分割
print(str)
delete_idx_in_page_i = page_to_remove[i]
for idx,s in enumerate(str):
if(idx in delete_idx_in_page_i or idx-len(str) in delete_idx_in_page_i):
continue
else:
result += s+"\n"
return result[:-1] #最后多写入了一个换行符,去掉
if __name__ == '__main__':
dict1 = {
'001.jpg': '这里是页眉\nahhfadvdajv\n\n1',
'002.jpg': '这里是页眉\n啊甘好难过好\n但是不管你会给你\n2',
'003.jpg': '这里是页眉\n北京公司\n技术同\n甲方:(委托人)\n乙方:(受托人)\n根据《中华人民共和国合同法》的规定,XXXX\n3',
'004.jpg': '这里是页眉\n北京有限公司\nsdivndjsdvnjsd\n\n4'
}
result = remove_run(dict1)
print(result)
码字不易,期待您的关注!后台回复【关键词】免费领取本人上传至CSDN的所有资料~