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
macOS Catalina
$ brew install poppler
$ brew install tesseract
$ brew install tesseract-lang
$ python3 src/watch.py input_path [output_path] [*extensions]
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.
Je l'ai assemblé dans le flux suivant.
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.
document officiel de watchdog
Qiita
Exécution de la commande déclenchée par la mise à jour du fichier (édition python)
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.
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.
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('-', '')
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('-', '')
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.
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.
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