оформи readme для проекта на github, делай так, как это обычно делают другие разработчики: 

from PIL import Image
from pdf2image import convert_from_path
import pandas as pd

class FileHandler():
    @staticmethod
    def read_pdf(path_to_pdf: str, dpi=300) -> list[Image.Image]:
        images = convert_from_path(path_to_pdf, dpi)
        return images
    
    @staticmethod
    def save_to_excel(output_path: str, tables: list[pd.DataFrame]) -> None:
        with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
            for i, table in enumerate(tables):
                table.to_excel(writer, sheet_name=f"{i+1}", index=False, header=False)


import cv2
import numpy as np

class ImageProcessor():
    @staticmethod
    def __detect_rotation(image):        
        edges = cv2.Canny(image, 50, 150, apertureSize=3)
        
        lines = cv2.HoughLines(edges, 1, np.pi/180, 200)
        
        if lines is not None:
            angles = []
            for rho, theta in lines[:, 0]:
                angle = np.degrees(theta) - 90
                if -45 <= angle <= 45:
                    angles.append(angle)
            
            if angles:
                median_angle = np.median(angles)
                return median_angle
        return 0
    
    @staticmethod
    def __rotate_image(image, angle):
        if angle == 0:
            return image
        
        h, w = image.shape[:2]
        center = (w // 2, h // 2)
        
        M = cv2.getRotationMatrix2D(center, angle, 1.0)
        rotated = cv2.warpAffine(
            image, M, (w, h), 
            flags=cv2.INTER_CUBIC, 
            borderMode=cv2.BORDER_REPLICATE
        )
        
        return rotated
    
    @staticmethod
    def tight_crop(img, pad=0):
        H, W = img.shape[:2]

        num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(
            img, connectivity=8
        )

        mask = np.zeros_like(img)

        for i in range(1, num_labels):
            area = stats[i, cv2.CC_STAT_AREA]
            if area > 25:
                mask[labels == i] = 255

        img = mask

        coords = cv2.findNonZero(img) 
        x, y, w, h = cv2.boundingRect(coords)

        x0 = max(x, 0)
        y0 = max(y, 0)
        x1 = min(x + w, W)
        y1 = min(y + h, H)

        img = cv2.copyMakeBorder(
            img[y0:y1, x0:x1], 
            pad, pad, pad, pad, 
            cv2.BORDER_CONSTANT,
            value=0
        )

        return img
    
    @staticmethod
    def process(image):
        image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2GRAY)

        image = cv2.adaptiveThreshold(
            src=image,
            maxValue=255,
            adaptiveMethod=cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
            thresholdType=cv2.THRESH_BINARY_INV,
            blockSize=19,
            C=10
        )        

        rotation_angle = ImageProcessor.__detect_rotation(image)
        if abs(rotation_angle) > 1:
            return ImageProcessor.__rotate_image(image, rotation_angle)
        return image

import pandas as pd

class PostProcessor():
    def __init__(
        self,
        index_col_pos=0,
        fix_index=True,
        separate_policy = "table", # "page", "table" "none"
        has_header_on_new_page=True,
    ):
        self.index_col_pos=index_col_pos
        self.fix_index = fix_index
        self.separate_policy = separate_policy
        self.has_header_on_new_page = has_header_on_new_page

    def __isNewTable(self, index_list: pd.Series) -> bool:
        try:
            numeric_values = pd.to_numeric(index_list, errors='coerce')
            return ((numeric_values >= 1) & (numeric_values <= 3)).any()
        except:
            return False
    
    def __separate_by_page(self, dfs) -> list[pd.DataFrame]:
        return dfs
    
    def __separate_by_table(self, dfs) -> list[pd.DataFrame]:
        table = pd.DataFrame()
        tables = []
        for df in dfs:
            if self.__isNewTable(df[df.columns[0]]):
                if not table.empty:
                    tables.append(table)
                table = df
            else:
                if self.has_header_on_new_page:
                    df = df.iloc[1:]
                table = pd.concat([table, df])    
        tables.append(table)
        return tables
    
    def __separate_none(self, dfs) -> list[pd.DataFrame]:
        table = pd.DataFrame()
        for i, df in enumerate(dfs):
            if i == 0:
                table = df
            else:
                if self.has_header_on_new_page:
                    df = df.iloc[1:]
                table = pd.concat([table, df])    
        return [table]
    
    def process(self, raw_tables):
        dfs = []
        for table in raw_tables:
            df = pd.DataFrame(table)
            df = df.dropna(how='all', axis=0).dropna(how='all', axis=1)

            if df.empty:
                continue  
            dfs.append(df)
        
        if self.separate_policy == "page":
            dfs = self.__separate_by_page(dfs)
        elif self.separate_policy == "table":
            dfs = self.__separate_by_table(dfs)
        else:
            dfs = self.__separate_none(dfs)

        return dfs


import easyocr
from .image_processor import ImageProcessor

class Reader():
    digit_list = "0123456789"
    char_list ="абвгдеёжзийклмнопрстуфхцчшщьыъэюяАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯ.-№ "
    dchar_list = char_list + digit_list + "()%/"
    def __init__(self, lang_list=["ru", "en"]):
        self.reader = easyocr.Reader(lang_list=lang_list)
        

    def read(self, cells, char_columns=[], num_columns=[]):
        data = []
        for row_num, row in enumerate(cells):
            row_data = []
            for i, cell_image in enumerate(row):
                cell_image = ImageProcessor.tight_crop(cell_image, pad=5)

                allowlist = self.dchar_list
                if i in char_columns:
                    allowlist=self.char_list
                elif i in num_columns:
                    allowlist = self.digit_list
                if row_num == 0:
                    allowlist = None

                result = self.reader.readtext(
                    cell_image, 
                    paragraph=True, 
                    allowlist=allowlist, 
                    detail=0, 
                    mag_ratio=1.2,
                )
                text = result[0] if len(result) > 0 else ""
                row_data.append(text)

            data.append(row_data)

        return data


import cv2
import numpy as np
from .image_processor import ImageProcessor

class TableProcessor():
    @staticmethod
    def __detect_tables(image):
        h, w = image.shape[:2]
        
        hor_kernel_length = 40
        ver_kernel_length = 50

        horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (hor_kernel_length, 1))
        vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, ver_kernel_length))
        
        horizontal_lines = cv2.morphologyEx(image, cv2.MORPH_OPEN, horizontal_kernel)
        vertical_lines = cv2.morphologyEx(image, cv2.MORPH_OPEN, vertical_kernel)
        
        table_mask = cv2.add(horizontal_lines, vertical_lines)
        
        kernel_close = cv2.getStructuringElement(cv2.MORPH_RECT, (20, 1))
        table_mask = cv2.morphologyEx(table_mask, cv2.MORPH_CLOSE, kernel_close)
        kernel_close = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 20))
        table_mask = cv2.morphologyEx(table_mask, cv2.MORPH_CLOSE, kernel_close)

        contours, _ = cv2.findContours(table_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        
        tables = []
        min_area = w * h * 0.01 


        dilation_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 9))
        table_mask = cv2.dilate(table_mask, dilation_kernel)


        img_without_lines = image.copy()
        img_without_lines[table_mask > 0] = 0
        
        for contour in contours:
            x, y, w_cont, h_cont = cv2.boundingRect(contour)
            area = w_cont * h_cont
            
            if area > min_area:  
                table_image = image[y:y + h_cont, x:x + w_cont]
                tables.append((table_image, img_without_lines, (x, y)))
        
        tables.sort(key=lambda x: x[2][1])
        return tables
    
    @staticmethod
    def __extract_table_structure(table_image):
        h, w = table_image.shape[:2]

        horizontal_kernel = cv2.getStructuringElement(
            cv2.MORPH_RECT, 
            (max(w // 10, 10), 1)
        )

        vertical_kernel = cv2.getStructuringElement(
            cv2.MORPH_RECT, 
            (1, max(h // 10, 10))
        )
        
        horizontal = cv2.morphologyEx(table_image, cv2.MORPH_OPEN, horizontal_kernel)
        vertical = cv2.morphologyEx(table_image, cv2.MORPH_OPEN, vertical_kernel)
        
        return TableProcessor.__get_cells_from_lines(horizontal, vertical, w, h)
    
    @staticmethod
    def __get_cells_from_lines(horizontal, vertical, width, height):
        h_lines = []
        h_projection = np.sum(horizontal, axis=1)
        h_positions = np.where(h_projection > width * 0.5)[0]
        
        if len(h_positions) > 0:
            h_lines.append(h_positions[0])
            for i in range(1, len(h_positions)):
                if h_positions[i] - h_positions[i-1] > 5:
                    h_lines.append(h_positions[i])
        
        v_lines = []
        v_projection = np.sum(vertical, axis=0)
        v_positions = np.where(v_projection > height * 0.5)[0]
        
        if len(v_positions) > 0:
            v_lines.append(v_positions[0])
            for i in range(1, len(v_positions)):
                if v_positions[i] - v_positions[i-1] > 5:
                    v_lines.append(v_positions[i])
        
        cells = []
        for i in range(len(h_lines) - 1):
            row_cells = []
            for j in range(len(v_lines) - 1):
                cell = (v_lines[j], h_lines[i], 
                       v_lines[j+1] - v_lines[j], 
                       h_lines[i+1] - h_lines[i])
                row_cells.append(cell)
            cells.append(row_cells)
        
        return cells
    
    @staticmethod
    def get_cells(image):
        all_cells = []

        for table_image, img_without_lines, table_offset in TableProcessor.__detect_tables(image):
            cells = TableProcessor.__extract_table_structure(table_image)

            offset_x, offset_y = table_offset
            adjusted_cells = []
            for row in cells:
                adjusted_row = []
                for cell in row:
                    x, y, w, h = cell
                    x += offset_x
                    y += offset_y

                    img_h, img_w = img_without_lines.shape[:2]
                    adjusted_cell = img_without_lines[
                        max(0, y - 2) : min(img_h, y + h + 2),
                        max(0, x - 2) : min(img_w, x + w + 2),
                    ]
                    adjusted_row.append(adjusted_cell)
                adjusted_cells.append(adjusted_row)
            all_cells.append(adjusted_cells)

        return all_cells

from .file_handler import FileHandler
from .reader import Reader
from .post_processor import PostProcessor
from .image_processor import ImageProcessor
from .table_processor import TableProcessor
from tqdm import tqdm

class Extractor():
    def __init__(
            self, 
            lang_list=["ru", "en"],
        ):

        self.__reader = Reader(
            lang_list=lang_list, 
        )
        
        self.__post_processor = PostProcessor(
            index_col_pos=0,
            fix_index=True,
            separate_policy="table",
            has_header_on_new_page=True,
        )
        
    def extract(self, path_to_pdf: str, path_to_xlsx: str, char_columns=[], num_columns=[], dpi=300):
        images = FileHandler.read_pdf(path_to_pdf, dpi)

        tables = []
        for image in tqdm(images):
            processed_image = ImageProcessor.process(image)

            cells_array = TableProcessor.get_cells(processed_image)

            for cells in cells_array:
                table = self.__reader.read(cells, char_columns, num_columns)
                if table and any(any(cell.strip() for cell in row) for row in table):
                    tables.append(table)

        tables = self.__post_processor.process(tables)

        FileHandler.save_to_excel(path_to_xlsx, tables)


from pdf2xlsx import Extractor

if __name__ == "__main__":
    extractor = Extractor(
        lang_list=["ru"],
    )
    extractor.extract(
        "./pdfs/d.pdf",
        "./xlsx/tables.xlsx",
        char_columns=[1, 2],
        num_columns=[0, 5],
        dpi=300,
    )