[PYTHON] Une histoire d'essayer d'automatiser un chot lorsque vous cuisinez vous-même

Aperçu

Il surveille le chemin du fichier spécifié et si vous y placez un fichier PDF, il renommera automatiquement le fichier PDF en titre du livre. :octocat:book_maker

Opération confirmée OS

macOS Catalina

Les choses nécessaires

  $ brew install poppler
  $ brew install tesseract
  $ brew install tesseract-lang

Comment utiliser

$ python3 src/watch.py input_path [output_path] [*extensions]

Pourquoi fait

Je me suis précipité pour acheter une machine de découpe et un scanner car je voulais digérer un grand nombre de livres chez mes parents. Cependant, j'ai souvent entendu dire que l'auto-restauration était gênant, alors je voulais atteindre un certain degré d'automatisation, j'ai donc créé ce programme.

Flux de travail

Je l'ai assemblé dans le flux suivant.

  1. Spécifiez le répertoire à surveiller et démarrez src / watch.py
  2. Placez le PDF dans le répertoire surveillé
  3. Détectez l'événement et obtenez le code ISBN à partir du contenu du fichier PDF
  1. Obtenez des informations sur les livres de chaque API en fonction de l'ISBN --API que vous utilisez
  2. Corrigez le nom du fichier et déplacez le fichier PDF vers le répertoire de sortie

Surveiller un répertoire spécifique

J'ai utilisé une bibliothèque appelée watchdog pour surveiller constamment le répertoire. Les documents et articles suivants ont été très utiles pour une utilisation détaillée de «watchdog». Merci beaucoup.


Maintenant, pour utiliser watchdog, vous avez besoin de Handler et ʻObserver. Handler décrit ce qu'il faut faire et comment gérer chaque événement (créer / supprimer / déplacer / modifier). Cette fois, seule la fonction «on_created», qui est l'événement au moment de la création, est définie. Cette méthode ʻon_created remplace la méthode de la classe FileSystemEventHandler dans watchdog.event.

src/handler/handler.py


from watchdog.events import PatternMatchingEventHandler

class Handler(PatternMatchingEventHandler):
    def __init__(self, input_path, output_path, patterns=None):
        if patterns is None:
            patterns = ['*.pdf']
        super(Handler, self).__init__(patterns=patterns,
                                      ignore_directories=True,
                                      case_sensitive=False)

    def on_created(self, event):
        #Faire quelque chose

Il définit une classe Handler et hérite de PatternMatchingEventHandler qui active la correspondance de modèles. En utilisant cela, vous pouvez limiter les types de fichiers qui sont détectés par les événements. Il existe également un RegexMatchingEventHandler qui vous permet d'utiliser des modèles d'expressions régulières. Cette fois, je voulais effectuer un traitement limité au PDF uniquement, j'ai donc défini patterns = ['* .pdf']. J'ai mis ʻignore_directories = Truepour ignorer le répertoire, et je voulais être capable de détecter à la fois* .pdf et * .PDF, donc j'ai mis case_sensitive = False`.


Ensuite, nous préparerons ʻObserver`, qui est responsable de la surveillance du gestionnaire.

src/watch.py


from watchdog.observers import Observer
from src.handler.handler import Handler


def watch(input_path, output_path, extensions):
    print([f'*.{extension}' for extension in extensions], flush=True)
    event_handler = Handler(input_path=input_path,
                            output_path=output_path,
                            patterns=[f'*.{extension}' for extension in extensions])
    observer = Observer()
    observer.schedule(event_handler, input_path, recursive=False)
    observer.start()
    print('--Start Observer--', flush=True)
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.unschedule_all()
        observer.stop()
        print('--End Observer--', flush=True)
    observer.join()

Dans l'objet Observer créé, indiquez s'il faut surveiller l'objet Handler, le répertoire surveillé et les sous-répertoires de manière récursive, puis créez-le. Démarrez la surveillance avec ʻobserver.start () et continuez à fonctionner avec l'instruction while et time.sleep (1) pour continuer le traitement. Quand Ctrl + C est pressé, ʻobserver.unschedule_all () termine toute surveillance, détache le gestionnaire d'événements, et ʻobserver.stop () notifie le thread de l'arrêt. Enfin, ʻobserver.join () fait attendre la fin du thread.

Obtenez le code ISBN à partir du code à barres à l'aide du shell

J'ai fait référence à ce blog. Merci beaucoup.

Lorsque vous obtenez le code ISBN, essayez de l'obtenir à partir du code à barres. Ceux que j'ai utilisés pour obtenir les informations du PDF sont pdfinfo, pdfimages et zbarimg. pdfinfo est d'obtenir le nombre total de pages dans le PDF. pdfimages consiste à créer uniquement les première et dernière pages jpeg en fonction du nombre total de pages obtenues à partir de pdfinfo. zbarimg a été utilisé pour obtenir le code ISBN à partir du jpeg généré par pdfimages.

getISBN.sh


#!/bin/bash

# Number of pages to check in PDF
PAGE_COUNT=1
# File path
FILE_PATH="$1"

# If the file extension is not pdf
shopt -s nocasematch
if [[ ! $1 =~ .+(\.pdf)$ ]]; then
  exit 1
fi
shopt -u nocasematch

# Delete all .image* generated by pdfimages
rm -f .image*

# Get total count of PDF pages
pages=$(pdfinfo "$FILE_PATH" | grep -E "^Pages" | sed -E "s/^Pages: +//") &&
# Generate JPEG from PDF
pdfimages -j -l "$PAGE_COUNT" "$FILE_PATH" .image_h &&
pdfimages -j -f $((pages - PAGE_COUNT)) "$FILE_PATH" .image_t &&
# Grep ISBN
isbnTitle="$(zbarimg -q .image* | sort | uniq | grep -E '^EAN-13:978' | sed -E 's/^EAN-13://' | sed 's/-//')" &&
# If the ISBN was found, echo the ISBN
[ "$isbnTitle" != "" ] &&
echo "$isbnTitle" && rm -f .image* && exit 0 ||
# Else, exit with error code
rm -f .image* && exit 1

Enfin, lorsque le code ISBN est obtenu, ʻecho "$ isbnTitle" ʻest reçu comme sortie standard côté Python.

Aussi cela&&Ou||Je n'ai pas bien compris le sens, mais l'article suivant m'a été utile. Merci beaucoup.

Utilisez Python pour obtenir le code ISBN

Obtenir du code à barres

Pour obtenir à partir du code à barres, pdf2image pour obtenir une image du PDF, et pour obtenir à partir du code à barres, [pyzbar](https://github.com/NaturalHistoryMuseum/ pyzbar) a été utilisé.

Avec pdf2image, générez des images de jpeg pour 2 pages à partir de la dernière page, appelezdecode ()avec pyzbar pour ces images et utilisez le modèle d'expression régulière du code ISBN ( S'il existe une chaîne de caractères correspondant à ^ 978), elle est renvoyée.

J'ai utilisé TemporaryDirectory () parce que je voulais que le répertoire place les images générées comme temporaires.

src/isbn_from_pdf.py


import re
import sys
import tempfile
import subprocess
from pyzbar.pyzbar import decode
from pdf2image import convert_from_path

input_path = input_path
texts = []
cmd = f'echo $(pdfinfo "{input_path}" | grep -E "^Pages" | sed -E "s/^Pages: +//")'
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
total_page_count = int(result.stdout.strip())

with tempfile.TemporaryDirectory() as temp_path:
    last_pages = convert_from_path(input_path,
                                    first_page=total_page_count - PAGE_COUNT,
                                    output_folder=temp_path,
                                    fmt='jpeg')
    # extract ISBN from using barcode
    for page in last_pages:
        decoded_data = decode(page)
        for data in decoded_data:
            if re.match('978', data[0].decode('utf-8', 'ignore')):
                return data[0].decode('utf-8', 'ignore').replace('-', '')

Récupérer du texte

Une autre option consiste à extraire le code ISBN de la dernière page du livre, qui contient des informations telles que l'éditeur et l'édition du livre.

J'ai utilisé pyocr pour extraire la chaîne de l'image. Pour utiliser pyocr, vous avez besoin de l'outil OCR, vous devez donc installer [tesseract] de Google (https://github.com/tesseract-ocr/tesseract).

src/isbn_from_pdf.py


import re
import sys
import pyocr
import tempfile
import subprocess
import pyocr.builders
from pdf2image import convert_from_path

input_path = input_path
texts = []
cmd = f'echo $(pdfinfo "{input_path}" | grep -E "^Pages" | sed -E "s/^Pages: +//")'
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
total_page_count = int(result.stdout.strip())

with tempfile.TemporaryDirectory() as temp_path:
    last_pages = convert_from_path(input_path,
                                    first_page=total_page_count - PAGE_COUNT,
                                    output_folder=temp_path,
                                    fmt='jpeg')
    tools = pyocr.get_available_tools()
    if len(tools) == 0:
        print('[ERROR] No OCR tool found.', flush=True)
        sys.exit()

    # convert image to string and extract ISBN
    tool = tools[0]
    lang = 'jpn'
    for page in last_pages:
        text = tool.image_to_string(
            page,
            lang=lang,
            builder=pyocr.builders.TextBuilder(tesseract_layout=3)
        )
        texts.append(text)
    for text in texts:
        if re.search(r'ISBN978-[0-4]-[0-9]{4}-[0-9]{4}-[0-9]', text):
            return re.findall(r'978-[0-4]-[0-9]{4}-[0-9]{4}-[0-9]', text).pop().replace('-', '')

Obtenez des informations sur les livres de chaque API

Pour obtenir les informations du livre, j'ai utilisé deux des API Google Livres et openBD. fait.

Les deux peuvent être obtenus au format JSON, mais comme ils ont des formes différentes, je voulais écrire du code aussi commun que possible, j'ai donc utilisé une bibliothèque appelée Box. J'ai fait.

Box est destiné à vous permettre d'obtenir ce que vous obtiendriez normalement avec dict.get ('key') ʻet dict ['key'] avec dict.key.another_key. .. Vous pouvez également utiliser dict ['key']`.

D'autres fonctionnalités incluent la possibilité pour key de convertir un cas de chameau ( camelCase) en une convention de dénomination Python pour les cas de serpent (snake_case), et pour key d'être un espace comme pensées personnelles. Il existe également une fonction pratique qui vous permet d'y accéder comme dict.personal_ought quand il y en a.

Vous trouverez ci-dessous le code pour obtenir de ʻopenBD`.

src/bookinfo_from_isbn.py


import re
import json
import requests
from box import Box

OPENBD_API_URL = 'https://api.openbd.jp/v1/get?isbn={}'

HEADERS = {"content-type": "application/json"}

class BookInfo:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def __str__(self):
        return f'<{self.__class__.__name__}>{json.dumps(self.__dict__, indent=4, ensure_ascii=False)}'


def _format_title(title):
    #Remplacez les crochets pleine largeur et les espaces pleine largeur par des espaces demi-largeur
    title = re.sub('[() ]', ' ', title).rstrip()
    #Remplacez un ou plusieurs espaces demi-largeur par un
    return re.sub(' +', ' ', title)


def _format_author(author):
    #Supprimer la chaîne de caractères après / écrite
    return re.sub('/.+', '', author)


def book_info_from_openbd(isbn):
    res = requests.get(OPENBD_API_URL.format(isbn), headers=HEADERS)
    if res.status_code == 200:
        openbd_res = Box(res.json()[0], camel_killer_box=True, default_box=True, default_box_attr='')
        if openbd_res is not None:
            open_bd_summary = openbd_res.summary
            title = _format_title(open_bd_summary.title)
            author = _format_author(open_bd_summary.author)
            return BookInfo(title=title, author=author)
    else:
        print(f'[WARNING] openBD status code was {res.status_code}', flush=True)

Puisque le titre du livre acquis et les informations de l'auteur sont mélangés avec pleine largeur et demi-largeur, nous avons préparé une fonction pour corriger chacun. (_format_title _ format_author) Je ne l'ai pas encore essayé, donc ces fonctions devront être ajustées.

Dans Box, camel_killer_box = True, qui convertit un cas de chameau en cas de serpent, et default_box = True et default_box_attr = '' `, même s'il n'y a pas de valeur.

Corrigez le nom du fichier et déplacez-vous vers le répertoire approprié

Tout d'abord, lorsque vous le démarrez, assurez-vous de créer un dossier à déplacer après avoir renommé le PDF.

src/handler/handler.py


import os
import datetime
from watchdog.events import PatternMatchingEventHandler

class Handler(PatternMatchingEventHandler):
    def __init__(self, input_path, output_path, patterns=None):
        if patterns is None:
            patterns = ['*.pdf']
        super(Handler, self).__init__(patterns=patterns,
                                      ignore_directories=True,
                                      case_sensitive=False)
        self.input_path = input_path
        # If the output_path is equal to input_path, then make a directory named with current time
        if input_path == output_path:
            self.output_path = os.path.join(self.input_path, datetime.datetime.now().strftime('%Y%m%d_%H%M%S'))
        else:
            self.output_path = output_path
        os.makedirs(self.output_path, exist_ok=True)

        # Create tmp directory inside of output directory
        self.tmp_path = os.path.join(self.output_path, 'tmp')
        os.makedirs(self.tmp_path, exist_ok=True)

Lorsque le processus démarre, il crée un dossier de destination formaté avec la date du jour ou un dossier de destination spécifié. Ensuite, créez un dossier tmp dans le dossier de destination de sortie à placer lorsqu'une erreur se produit (lorsqu'il y a le même livre PDF, quand ISBN n'est pas trouvé, lorsqu'il n'y a pas d'informations sur le livre). ..


src/handler/handler.py


    def __del__(self):
        # Delete the tmp directory, when the directory is empty
        tmp_files_len = len(os.listdir(self.tmp_path))
        if tmp_files_len == 0:
            os.rmdir(self.tmp_path)

        # Delete the output directory, when the directory is empty
        output_files_len = len(os.listdir(self.output_path))
        if output_files_len == 0:
            os.rmdir(self.output_path)

Lorsque le processus est terminé, décrivez la méthode __del__ pour que s'il y a un fichier dans le dossier de destination de sortie / dossier tmp, il sera laissé, et s'il n'existe pas, il sera supprimé.


src/handler/handler.py


import shutil
import subprocess
from src.isbn_from_pdf import get_isbn_from_pdf, NoSuchISBNException
from src.bookinfo_from_isbn import book_info_from_google, book_info_from_openbd, NoSuchBookInfoException

    def on_created(self, event):
        print('!Create Event!', flush=True)
        shell_path = os.path.join(os.path.dirname(__file__), '../../getISBN.sh')
        event_src_path = event.src_path
        cmd = f'{shell_path} {event_src_path}'
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
        try:
            if result.returncode == 0:
                # Retrieve ISBN from shell
                isbn = result.stdout.strip()
                print(f'ISBN from Shell -> {isbn}', flush=True)
                self._book_info_from_each_api(isbn, event_src_path)

            else:
                # Get ISBN from pdf barcode or text
                isbn = get_isbn_from_pdf(event_src_path)
                print(f'ISBN from Python -> {isbn}', flush=True)
                self._book_info_from_each_api(isbn, event_src_path)

        except (NoSuchISBNException, NoSuchBookInfoException) as e:
            print(e.args[0], flush=True)
            shutil.move(event_src_path, self.tmp_path)
            print(f'Move {os.path.basename(event_src_path)} to {self.tmp_path}', flush=True)

La méthode «on_created» décrit le flux global du flux de travail.

Lors de l'exécution du shell, exécutez le shell avec subprocess.run () pour recevoir la sortie standard, recevoir l'état du shell de result.returncode, et recevoir la sortie standard avec result.stdout. Peut être fait

En outre, lors de la récupération des informations du livre à partir du code ISBN, une exception spéciale est levée.

Résumé

Merci d'avoir lu jusqu'ici. J'avais du mal avec l'endroit où démarrer la commande et le nom de la variable / nom de la fonction, mais j'ai réussi à en faire la forme minimale. À ce stade, seul le format PDF est pris en charge, mais j'aimerais pouvoir prendre en charge epub. Je veux aussi pouvoir le faire sous Windows.

S'il y a des fautes de frappe ou des erreurs, c'est la voie à suivre! S'il vous plaît laissez-moi savoir si vous en avez. Merci beaucoup.

Recommended Posts

Une histoire d'essayer d'automatiser un chot lorsque vous cuisinez vous-même
Une histoire sur la tentative d'implémentation de variables privées en Python.
Une histoire sur la tentative d'exécuter plusieurs versions de Python (édition Mac)
Une histoire d'essayer d'exécuter JavaScripthon sur Windows et d'abandonner.
L'histoire de l'abandon d'essayer de se connecter à MySQL en utilisant Heroku
Une histoire sur un débutant essayant de configurer CentOS 8 (mémo de procédure)
Une histoire qui a souffert d'une différence de système d'exploitation lors de la tentative d'implémentation d'un article
Une histoire d'essayer d'améliorer le processus de test d'un système vieux de 20 ans écrit en C
Une histoire d'essayer d'installer uwsgi sur une instance EC2 et d'échouer
Une histoire qui nécessitait des préparatifs pour essayer de faire un tutoriel Django avec des centos simples
Une histoire qui a échoué lors de la tentative de suppression du suffixe d'une chaîne avec rstrip
Une histoire bloquée lors de la tentative de mise à niveau de la version Python avec GCE
Une histoire d'essayer un monorepo (Golang +) Python avec Bazel
Une histoire sur un débutant Python essayant d'obtenir des résultats de recherche Google à l'aide de l'API
Une histoire sur la difficulté à traiter en boucle 3 millions de données d'identification
Lorsque vous voulez plt.save dans l'instruction for
Une histoire sur la tentative d'introduire Linter au milieu d'un projet Python (Flask)
[Note] Une histoire sur la tentative de remplacer une méthode de classe avec deux barres inférieures dans la série Python 3.
[Django] Une histoire sur le fait de rester coincé dans un marais en essayant de valider un zip avec un formulaire [TDD]
Une histoire sur la façon de spécifier un chemin relatif en python.
[python] Remarques lors de la tentative d'utilisation de numpy avec Cython
Une histoire sur la façon de traiter le problème CORS
Une histoire sur une guerre lorsque deux nouveaux arrivants ont développé une application
L'histoire d'un ingénieur directeur de 40 ans qui réussit "Deep Learning for ENGINEER"
Une histoire dans laquelle l'algorithme est arrivé à une conclusion ridicule en essayant de résoudre correctement le problème du voyageur de commerce
À propos de l'erreur que j'ai rencontrée en essayant d'utiliser Adafruit_DHT à partir de Python sur Raspberry Pi
Une histoire sur l'ajout d'une API REST à un démon créé avec Python
Une histoire sur le fait de vouloir penser à des personnages déformés dans GAE / P
Une histoire sur la tentative de reproduire Katsuo Isono, qui ne réagit pas aux inconvénients, par traitement du langage naturel.
[Pour les débutants chez AtCoder] Parlez de la quantité de calcul que vous voulez connaître approximativement
Une histoire où un débutant est coincé en essayant de créer un environnement de plug-in vim 8.2 + python 3.8.2 + lua sur Ubuntu 18.04.4 LTS
Une histoire rafraîchissante sur Slice en Python
Une histoire de mauvaise humeur sur Slice en Python
Une histoire accro aux pipelines Azure
UnicodeEncodeError lors de la tentative d'exécution du radon
L'histoire de l'utilisation de la réduction de Python
Points à surveiller lors de la création d'un environnement Python sur un Mac
Une histoire à laquelle j'étais accro à essayer d'installer LightFM sur Amazon Linux
Une histoire à laquelle j'étais accro à essayer d'obtenir une URL de vidéo avec tweepy
[Mémorandum] Une histoire sur l'essai du didacticiel OpenCV (reconnaissance faciale) dans un environnement Windows
L'histoire d'un débutant en apprentissage profond essayant de classer les guitares avec CNN
J'obtiens un UnicodeDecodeError en essayant de me connecter à oracle avec python sqlalchemy
Une note lors de la recherche d'une alternative aux pandas roulant pour une fenêtre en mouvement