当前所在位置: 首页 > 儿童教育 > 正文

基于Python实现PDF区域文本提取工具

2022-11-15 canyinms.com 【 字体:

功能简介

打开软件后界面如下:

点击打开文件按钮打开之前的PDF文件后效果如下:

框选区域后,标题栏会自动显示当前框选的区域提取到的文字,还可以左右按钮切换:

实际我们需要提取文字的区域可能不止这一个,所以程序支持多区域框选:

完成区域框选后就可以点击保存文件,将PDF每页提取到的文本保存到一个csv文件中,当前选区的保存结果如下:

可以看到已经按框选顺序依次保存了每一个区域的字符串。

如果选择区域时发现提取结果不准确,可以撤销后重新选择:

保存图片则会将PDF的每页的整体保存为一张图片,未选择区域时,以页码为文件名保存图片:

选择区域时,会自动提取最后一个区域提取的文本作为当前页的文件名:

开发代码

当然这个项目由于本人是一次使用wxpython,功能非常简约,现在将完整代码开源出来期待各位大佬的改进。

源码和已编译工具下载地址:

https://codechina.csdn.net/as604049322/python_gui

完整代码:

"""
小小明的代码
CSDN主页:https://blog.csdn.net/as604049322
"""
__author__ = '小小明'
__time__ = '2021/11/24'

import csv

import wx
import os
import fitz


class MyCanvas(wx.Panel):
    def __init__(self, parent):
        wx.Panel.__init__(self, parent)
        self.parent = parent
        self.rects = []
        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftButtonEvent)
        self.Bind(wx.EVT_LEFT_UP, self.OnLeftButtonEvent)
        self.Bind(wx.EVT_MOTION, self.OnLeftButtonEvent)
        self.Bind(wx.EVT_PAINT, self.DoDrawing)
        b = wx.Button(self, -1, "打开文件", (0, 0))
        self.Bind(wx.EVT_BUTTON, self.OnButton, b)
        b = wx.Button(self, -1, "保存文件", (75, 0))
        self.Bind(wx.EVT_BUTTON, self.save_file, b)
        b = wx.Button(self, -1, "保存图片", (150, 0))
        self.Bind(wx.EVT_BUTTON, self.save_img, b)
        b = wx.Button(self, -1, "撤销选区", (225, 0))
        self.Bind(wx.EVT_BUTTON, self.back_select, b)

        b = wx.Button(self, -1, "《", (300, 0), size=(25, 25))
        self.Bind(wx.EVT_BUTTON, self.previous, b)
        b = wx.Button(self, -1, "》", (325, 0), size=(25, 25))
        self.Bind(wx.EVT_BUTTON, self.next, b)

        self.g1 = wx.Gauge(self, -1, 100, (0, 30), (-1, 100), wx.GA_VERTICAL)

    def previous(self, evt):
        if not hasattr(self, "pdfDoc"):
            return
        if self.i > 0:
            self.i -= 1
            self.change_pdf_page(self.i, False)
            self.DoDrawing(-1)
            if self.rects:
                self.parent.SetTitle(self.path + "|" + self.extract_pdf_text())

    def next(self, evt):
        if not hasattr(self, "pdfDoc"):
            return
        if self.i < self.pageCount - 1:
            self.i += 1
            self.change_pdf_page(self.i, False)
            self.DoDrawing(-1)
            if self.rects:
                self.parent.SetTitle(self.path + "|" + self.extract_pdf_text())

    def back_select(self, evt):
        if self.rects:
            self.rects.pop()
            self.DoDrawing(-1)

    def OnButton(self, evt):
        dlg = wx.FileDialog(
            self, message="选择一个PDF文件",
            defaultDir=os.getcwd(),
            defaultFile="",
            wildcard="PDF文件(*.pdf)|*.pdf",
            style=wx.FD_OPEN | wx.FD_CHANGE_DIR |
                  wx.FD_FILE_MUST_EXIST | wx.FD_PREVIEW
        )
        if dlg.ShowModal() == wx.ID_OK:
            self.rects = []
            path = dlg.GetPath()
            self.pdfDoc = fitz.open(path)
            self.i = 0
            self.pageCount = self.pdfDoc.pageCount
            self.change_pdf_page(self.i)
            self.path = os.path.basename(path)
            self.parent.SetTitle(self.path)
            self.DoDrawing(-1)
        dlg.Destroy()

    def change_pdf_page(self, i, move=True):
        page = self.pdfDoc[i]
        rect = page.rect
        print("pdf范围:", rect)
        mat = fitz.Matrix(1, 1)
        pix = page.get_pixmap(matrix=mat, alpha=False, clip=rect)
        pix.save("tmp.png")
        self.change_img("tmp.png", move)

    def save_FileDialog(self, format="csv"):
        dlg = wx.FileDialog(
            self, message=f"保存一个{format}文件", defaultDir=os.getcwd(),
            defaultFile="", wildcard=f"{format}文件(*.{format})|*.{format}", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT
        )
        path = None
        if dlg.ShowModal() == wx.ID_OK:
            path = dlg.GetPath()
        dlg.Destroy()
        return path

    def save_img(self, evt):
        if not hasattr(self, "pdfDoc"):
            return
        dlg = wx.DirDialog(self, "选择图片保存的文件夹:",
                           style=wx.DD_DEFAULT_STYLE
                           # | wx.DD_DIR_MUST_EXIST
                           # | wx.DD_CHANGE_DIR
                           )
        mat = fitz.Matrix(1, 1)
        if dlg.ShowModal() == wx.ID_OK:
            path = dlg.GetPath()
            for i in range(self.pdfDoc.pageCount):
                page = self.pdfDoc[i]
                clip = page.rect
                pix = page.get_pixmap(matrix=mat, alpha=False, clip=clip)
                if self.rects:
                    name = self.extract_pdf_text(page=page, rect=self.rects[-1])
                else:
                    name = f"p{i:0>3d}"
                pix.save(f"{path}/{name}.png")
                self.g1.SetValue((i + 1) * 100 // self.pdfDoc.pageCount)
        dlg.Destroy()
        os.system(f"explorer {path}")

    def save_file(self, evt):
        if not hasattr(self, "pdfDoc"):
            return
        path = self.save_FileDialog()
        if path is None:
            return
        data = []
        for i in range(self.pdfDoc.pageCount):
            page = self.pdfDoc[i]
            row = [self.extract_pdf_text(page, rect)
                   for i, rect in enumerate(self.rects)]
            data.append(row)
        with open(path, "w") as f:
            writer = csv.writer(f, lineterminator="\n")
            row = [f"区域{i}" for i in range(1, len(row) + 1)]
            writer.writerow(row)
            for row in data:
                writer.writerow(row)
        os.system(f"cmd /c start {path}")

    def extract_pdf_text(self, page=None, rect=None):
        if page is None:
            page = self.pdfDoc[self.i]
        if rect is None:
            rect = self.rects[-1]
        a, b, c, d = rect
        clip = fitz.Rect(a, b, a + c, b + d)
        text = page.get_text(clip=clip).strip()
        return text

    def change_img(self, img_path, move=True):
        self.bmp = wx.Bitmap(img_path)
        self.SetSize(self.bmp.GetSize())
        self.parent.SetSize(self.parent.GetBestSize())
        if move:
            self.parent.Center()

    def DoDrawing(self, evt):
        if not hasattr(self, "bmp"):
            return
        dc = wx.ClientDC(self)
        dc.DrawBitmap(self.bmp, 0, 0, True)
        dc.SetPen(wx.Pen('blue'))
        dc.SetBrush(wx.Brush('white', wx.BRUSHSTYLE_TRANSPARENT))
        dc.DrawRectangleList(self.rects)

    def OnLeftButtonEvent(self, event):
        if event.LeftDown():
            self.x, self.y = event.GetPosition()
            self.rects.append([self.x, self.y, 0, 0])
        elif event.Dragging():
            x, y = event.GetPosition()
            self.rects[-1][2] = x - self.x
            self.rects[-1][3] = y - self.y
            self.DoDrawing(-1)
        elif event.LeftUp():
            print(self.rects)
            if self.rects[-1][2] < 5 or self.rects[-1][3] < 5:
                self.rects.pop()
            else:
                self.parent.SetTitle(self.path + "|" + self.extract_pdf_text())


app = wx.App()
frm = wx.Frame(None)
pnl = MyCanvas(frm)
frm.Center()
frm.Show()
frm.SetTitle("PDF文本提取器")
app.MainLoop()

阅读全文
相关推荐

橘子洲旅游攻略 长沙橘子洲怎么玩

橘子洲旅游攻略 长沙橘子洲怎么玩
橘子洲是湘江中的一个冲击沙洲,四面环水绵延数十里,是国家重点风景名胜区。景区内有历史文化陈列馆、潇湘名人会所、柑橘文化园等景观,集观赏休闲娱乐于一体。在特定时间段,橘子洲烟火是一场绝不可错过的视觉盛宴。景区原面积约17公顷,其中开放区面积约6公顷,主要景点有洲头颂橘亭、汉白玉诗词碑、铜像广场、藤架广场、揽岳亭、枕江亭、盆景园、大门广场等。

西安周边旅游攻略 西安周边最值得去的地方

西安周边旅游攻略 西安周边最值得去的地方
1、华山风景名胜区:“华山论剑谁为峰,一见重阳道成空”,武侠小说为华山披上了浓厚的江湖色彩,同时华山用它的险峻向每一位前来攀登的游客发起挑战。整个华山拥有多达200余处景点,东峰的日出、西峰的峭壁、南峰的林木、北峰的云雾,每一处景色都独具魅力。打卡建议:在西峰乘坐索道上山,再从北峰乘坐索道下山,一天游玩下来大致需要6—8个小时,并且可以把景点都逛到。如果选择夜爬华山看日出的话,记得带一件厚一点的衣服,越往上爬山上越冷。还有记得爬山戴双手套,因为华山有些路段需要双脚并用才能攀登的上去。

毛坦厂中学万人雨中送考 毛坦厂送考头车车牌91666祝福考生

毛坦厂中学万人雨中送考 毛坦厂送考头车车牌91666祝福考生
六安市毛坦厂中学,简称“毛中”,位于安徽省六安市金安区毛坦厂镇,被称为“亚洲最大高考工厂”。每次送考时节也都会有万余名家长和当地居民前来送考,毛坦厂镇的万人送考成为当地乃至安徽省内的一个“高考景观”。  毛坦厂中学万人雨中送考  6月5日,安徽六安。今早7点半,毛坦厂中学送考车队又一次出发,考生

珠峰被救女子不愿支付全部救援费用 珠峰攀登者为救遇险女子放弃登顶

珠峰被救女子不愿支付全部救援费用 珠峰攀登者为救遇险女子放弃登顶
近日,湖南省登山队2名队员范江涛、谢如祥在距离登顶珠峰不到400米时,为救一名同中国籍的女子放弃登顶。此事引起网友广泛关注,网友纷纷为两人的义举支持点赞。但在成功救人后,被救女子的态度和表现,让两人倍感寒心。为请求救女子,他们当时允诺给夏尔巴向导1万美元,然而该女子事后只愿承担4000美元。

​登珠峰获救女子只愿承担4成救援费是怎么回事 登珠峰获救女子只愿承担4成救援费具体情况

​登珠峰获救女子只愿承担4成救援费是怎么回事 登珠峰获救女子只愿承担4成救援费具体情况
近日,湖南省登山队2名队员为救人放弃珠峰登顶计划引起热议。3日,有网友发布消息称:被救者只愿承担4成救援费。具体登珠峰获救女子只愿承担4成救援费是怎么回事?一起来了解登珠峰获救女子只愿承担4成救援费具体情况。登珠峰获救女子只愿承担4成救援费是怎么回事近日,湖南省登山队2名队员在距离登顶珠峰不到400

国内出现偏肺病毒感染 尚无治疗药物

国内出现偏肺病毒感染 尚无治疗药物
5月31日,据美国疾控中心(CDC)的呼吸道病毒监测系统显示,今年春季以来,人类偏肺病毒(HMPV)在美国各地区呈现高发态势,该病毒正肆虐美国各大医院的重症监护病房和儿科医院。  6月3日,某三甲医院感染控制中心主任医师告诉记者:“根据近几年国内零散报告,人偏肺病毒在呼吸道感染病人中确实是存在的

男子连吃100天麦当劳减重53斤是真的吗 男子连吃100天麦当劳为什么能减重53斤

男子连吃100天麦当劳减重53斤是真的吗 男子连吃100天麦当劳为什么能减重53斤
近日,据说美国一57岁男子通过连吃100天麦当劳的方式减肥,减了53斤。那么,男子连吃100天麦当劳减重53斤是真的吗?到底男子连吃100天麦当劳为什么能减重53斤?一起来了解具体情况。男子连吃100天麦当劳减重53斤是真的吗近日,据外媒报道,美国一名57岁男子通过连吃100天麦当劳的方式减肥,体重

本周狗屎运最旺的星座 本周狗屎运最旺的四大星座

本周狗屎运最旺的星座 本周狗屎运最旺的四大星座
本周(6.5-6.11)狗屎运最旺的星座TOP4如下:  TOP4:巨蟹座  金星冲冥王,巨蟹座走的狗屎运,可以说是超级大坨!不光是财运一飞冲天,主要事业运+学业运,迎来双运加速的爆发期!  TOP3:摩羯座  月亮六合水星,紧接着拱水星,摩羯运势仿佛踩上了蹦蹦床!正财运和偏财运,简直装备了跃迁

印度耗资百亿卢比的大桥又塌了是什么情况 印度耗资百亿卢比的大桥又塌了是为什么

印度耗资百亿卢比的大桥又塌了是什么情况 印度耗资百亿卢比的大桥又塌了是为什么
据印度媒体当地时间6月4日报道,印度比哈尔邦在建的横跨恒河的阿格瓦尼-苏丹甘吉大桥再次发生部分坍塌。据悉,此桥耗资百亿卢比。具体印度耗资百亿卢比的大桥又塌了是什么情况?到底印度耗资百亿卢比的大桥又塌了是为什么?一起来了解。印度耗资百亿卢比的大桥又塌了是什么情况据印度媒体6月5日报道,上周日下午6点左

一个村考出300多大学生6个北大 湖南一小村考上一本奖4000二本3000

一个村考出300多大学生6个北大 湖南一小村考上一本奖4000二本3000
一个村考出300多大学生6个北大是怎么回事?湖南一小村考上一本奖4000二本3000是什么情况?湖南益阳,爱屋湾村:一个让人自豪的地方,也是一个让人感动的地方。这个小村庄,历经数十年积淀,已经培养出了不少人才。据统计,爱屋湾村十年来累计奖励学生68名,其中有6人考上了北京大学,5人留学哈佛,其他
本文Tag