教材の内容に関係のない質問や教材とは異なる環境・バージョンで進めている場合のエラーなど、教材に関係しない質問は推奨していないため回答できない場合がございます。
その場合、teratailなどの外部サイトを利用して質問することをおすすめします。教材の誤字脱字や追記・改善の要望は「文章の間違いや改善点の指摘」からお願いします。
第4章ではPDFファイルのアップロード機能を実装していきます。
また、アップロードされたPDFファイルからテキスト情報を抽出しExcelファイルへ情報を埋め込む処理も実装します。
まず最初に、全体の処理フローを解説します。
上図の通り①~⑧の順番で処理を実行します。
Copied!1. PDFをアップロード
2. pdfフォルダにアップロードされる(tutorial/media/pdf/xxxxxxxxxxx/AAA.pdf)
3. 予め用意してあるテンプレートファイルをtempフォルダにコピーして年月日時分秒つきのファイル名に変更
4. PDFからテキストデータを抽出
5. テキストデータの内容をExcelにはめ込む
6. 出来上がった請求書ファイルをユーザ毎の格納フォルダに移動
7. 請求書ファイルの完成
8. 一時フォルダを削除
①、②PDFのアップロード
まず最初に、請求書PDFファイルをアップロード画面から選択します。(PDFファイルは同時に複数選択可能とします)
ファイル選択後、「アップロード」ボタンを押すと、メディアファイルで設定したフォルダ(meida/pdf/xxxxxxxxxx)配下にファイルがアップロードされます。
アップロード先のフォルダはtutorial/media/pdfとし、その直下に任意の10文字で構成されたフォルダ名を自動的に生成します。
アップロードされたPDFファイルを一意なディレクトリに格納させることで、各処理の対象とするPDFファイルを特定することが容易になります。
※もし、すべてのPDFファイルを特定のフォルダにアップロードしてしまうと、どのユーザがいつアップロードしたPDFか判別できなくなってしまいます。
各々の処理がどのPDFファイルを処理対象とするかをプログラム上一意に特定させたいのでこのような設計にしています。
③テンプレートファイルを一時作業用フォルダへコピー
予め請求書一覧ファイル.xlsxという名前で以下のようなテンプレートファイルをtutorial/media/templateフォルダ配下に配置しておきます。
PDFがアップロードされたら一時作業用フォルダtutorial/media/tempに請求書一覧ファイル.xlsxをコピーします。
その後、年月日時分秒つきのファイル名(例:請求書一覧ファイル_20190724-195001.xlsx)に変更します。
④PDFからテキスト情報を抽出
pdfminer.sixというライブラリを使ってアップロードされたPDFファイルを解析してテキスト情報を抽出します。
複数ファイルがアップロードされていた場合は、1つずつPDFファイルを解析します。
⑤テキスト情報をExcelに埋め込む
抽出したテキストデータの内容を③で作成したExcelファイル(請求書一覧ファイル_yyyymmdd-hhmmss.xlsx)に埋め込みます。
⑥請求書ファイルをユーザフォルダに移動
出来上がった請求書ファイル(請求書一覧ファイル_yyyymmdd-hhmmss.xlsx)をユーザ毎の格納フォルダtutorial/media/excel/ユーザ名に移動させます。
⑧一時フォルダの削除
アップロード時に自動生成された一時フォルダtutorial/media/pdf/xxxxx xxxxxを削除して処理を完了します。
全体の処理内容は以上です。
本章はボリュームが多いため4-1~4-4の4つのパートに分かれています。
前半(4-1~4-2)では、PDFファイルをアップロードする機能を実装していきます。
後半(4-3~4-4)ではアップロードされたPDFファイルを解析してテキスト情報を抽出します。
さらに、Excelファイルに抽出した情報を埋め込んで最終的にExcel形式の請求書ファイルを生成する部分まで実装していきます。
PDFファイルのアップロード機能の作成(4-1~4-2)の流れは以下の通りです。
Copied!0. settings.pyの設定(メディアファイルの設定) #4-1
1. forms.pyの設定(アップロードフォームの定義) #4-1
2. urls.pyの定義(アップロード画面用URLパターンの定義) #4-1
3. views.pyの設定(アップロード機能の定義) #4-2
4. テンプレート設定(アップロード画面の定義) #4-2
本パート(4-1)では上記0~2までを実装していきます。
それではPDFファイルのアップロード機能を実装していきましょう。
Djangoでファイルアップロードを行う場合は、メディアファイルの設定を行う必要があります。
具体的には、settings.pyでアップロード先となるパスのルート設定(MEDIA_ROOT)とアップロードしたファイルのURLのルート設定(MEDIA_URL)を行う必要があります。
それでは、settings.pyを開いて、ファイルの末尾に以下の設定を追加しましょう。
Copied!tutorial
└── tutorial
└──settings.py #変更対象
追加するメディアファイルの設定
Copied!MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
MEDIA_ROOTは以下のようにDjangoプロジェクトフォルダ直下のmediaフォルダを指定しています。
Copied!..\tutorial\media
もう少し補足すると、BASE_DIR変数にはDjangoプロジェクトフォルダのパスが格納されています。
なぜかというと、settings.py内に以下の設定がされているからです。
Copied!BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
上記BASE_DIRにはDjangoプロジェクトフォルダの絶対パスが格納されているため、os.path.joinメソッドを使って「..\tutorial\media」というメディアファイルのパスを生成しています。
実際にBASE_DIRの設定内容を確認したい方は、Djangoのシェルモードを起動して以下の手順で確認してみましょう。
python1234 Copied!$ cd tutorial
$ python manage.py shell #シェルモードを起動
$ from tutorial.settings import * #settings.pyをインポート
$ print(BASE_DIR)
続いて、MEDIA_URLの設定値は'/media/'としています。
具体的には、http://127.0.0.1:8000/media を設定したことになります。
最後に、設定したメディアファイルのフォルダを作成しておきましょう。
python123456 Copied!../tutorial
├── accounts
├── pdfmr
├── templates
├── tutorial
└── media #新規作成
メディアファイルの設定は以上です。
まず最初に、PDFアップロード画面フォーム定義と単純なアップロード処理部分を開発していきます。
全体図の赤枠の部分です。
pdfmrアプリケーション直下にforms.pyを新規作成します。
python12345678910111213 Copied!../tutorial
├── accounts
├── pdfmr
| └── forms.py #新規作成
| ├── admin.py
| ├── apps.py
| ├── models.py
| ├── tests.py
| ├── urls.py
| ├── views.py
├── templates
├── manage.py
└── tutorial
ファイルを作成したら、以下のコードを記載してください。
全体コード
python12345678910111213141516171819202122232425262728 Copied!
from django import forms
from django.conf import settings
from django.core.files.storage import default_storage
import os, random, string
class UploadForm(forms.Form):
"""PDFアップロード用フォームの定義
saveメソッドはアップロードしたPDFを一時フォルダに保存する。
"""
document = forms.FileField(label="PDFアップロード",
widget=forms.ClearableFileInput(attrs={'multiple': True}),
)
def save(self):
upload_files = self.files.getlist('document')
temp_dir = os.path.join(settings.MEDIA_ROOT, self.create_dir(10)) #一時フォルダの生成
for pdf in upload_files:
default_storage.save(os.path.join(temp_dir, pdf.name), pdf) #一時フォルダにPDFを保存
return temp_dir
def create_dir(self, n):
"""一時フォルダ名生成関数"""
return 'pdf\\' + ''.join(random.choices(string.ascii_letters + string.digits, k=n))
まず、PDFアップロード用フォームを定義するためにforms.Formを承継してUploadFormという名称のクラスを定義します。
python1 Copied!class UploadForm(forms.Form):
今回は特にモデルの定義(models.py)は行いませんのでモデルフォーム(Models.Form)ではなく通常のフォーム(forms.Form)を利用します。
次の行ではdocumentという名称のアップロードフィールドを定義しています。
複数のファイルを選択してアップロード可能にするため、forms.FileFieldフィールドオプションで以下の設定を追加しています。
python123 Copied!document = forms.FileField(label="PDFアップロード",
widget=forms.ClearableFileInput(attrs={'multiple': True}),
)
上記のようにattrsでmultipleをTrueに設定することでhtmlのinputタグでmultipleという属性が付与され複数のファイルを選択してアップロードできるようになります。
ClearableFileInputは<input type="file" ...> のようにレンダリングしてくれるフォームウィジェットです。
DjangoのウィジェットについてはDjango公式ドキュメントにいろいろと記載されていますので、興味のある方は確認してみてください。
参考URL:https://docs.djangoproject.com/ja/2.2/ref/forms/widgets/
続いて、UploadFormクラス内にsaveメソッドを独自に定義します。
python123456 Copied!def save(self):
upload_files = self.files.getlist('document')
temp_dir = os.path.join(settings.MEDIA_ROOT, self.create_dir(10)) #一時フォルダの生成
for pdf in upload_files:
default_storage.save(os.path.join(temp_dir, pdf.name), pdf) #一時フォルダにPDFを保存
return temp_dir
forms.Formクラスにはsave()というメソッドは用意されていませんので、あくまで独自に実装するメソッドになります。
このsaveメソッド内には、PDFをアップロードした際にtutorial\media\pdf<ランダムな10個の文字列>にファイルが保存されるような処理を記載しています。
※これは独自に実装した処理内容ですので、メソッド名はsaveという名称でなくても可能ですが、ここではわかりやすくsaveという名称で定義しています。
【補足】
forms.Formは、Lib\site-packages\django\forms\forms.py内に以下のように定義されています。
python1234567 Copied!class Form(BaseForm, metaclass=DeclarativeFieldsMetaclass):
"A collection of Fields, plus their associated data."
# This is a separate class from BaseForm in order to abstract the way
# self.fields is specified. This class (Form) is the one that does the
# fancy metaclass stuff purely for the semantic sugar -- it allows one
# to define a form using declarative syntax.
# BaseForm itself has no way of designating self.fields.
上記の通り、Django標準のFormクラスはBaseFormというクラスを承継して作られていることがわかります。
さらに同ファイル内にBaseFormというクラスが存在していますが、このクラス内にsaveというメソッドは定義されていないため、「あくまで独自に実装したsaveメソッド」ということになります。
さて、saveメソッドの説明に戻ります。
以下のコードでフォーム画面上にアップロードされたPDFファイルをupload_files変数に格納します。
PDFファイルが複数アップロードされた場合は、このupload_files変数にすべてのPDFファイル情報が格納されます。
python1 Copied!upload_files = self.files.getlist('document')
ユーザーがアップロードしたファイルはrequest.FILESの中で管理されます。
フォーム側からはself.files.getlist('document')とすることでアップロードされたrequest.FILESにアクセスできます。
なお、files.getlistの()内にはUploadFormクラス内で定義した属性名documentを指定します。
つづいて、以下のコードではアップロード先となる任意の10文字のフォルダ名を自動生成しています。
python1 Copied!temp_dir = os.path.join(settings.MEDIA_ROOT, create_dir(10))
PDFのアップロード先はtutorial\media\pdf\任意の10文字という形で自動生成します。
具体例
Copied!tutorial\\media\\pdf\\BOqzkvHvYr'
settings.MEDIA_ROOTはsettings.pyで定義したMEDIA_ROOTの設定値(/media)です。
また、create_dir(n)は引数に整数nを受け取りrandom.choices関数とstringを使って任意のn文字の文字列を返す関数です。
python123 Copied!def create_dir(self, n):
"""一時フォルダ名生成関数"""
return 'pdf\\' + ''.join(random.choices(string.ascii_letters + string.digits, k=n))
補足
Copied!string.ascii_letters
⇒小文字 'abcdefghijklmnopqrstuvwxyz'、大文字 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'を合わせた文字列定数を表します。
string.digits
⇒文字列 '0123456789'を表します。
参考URL:https://docs.python.org/ja/3/library/string.html
最後に、以下のコードでアップロードされたPDFファイルを1件ずつ取得します。
そして、default_storageのsaveメソッドを使ってtutorial\media\pdf\ランダムな10個の文字列\pdfのファイル名としてファイルをアップロードします。
python123 Copied!for pdf in upload_files:
default_storage.save(os.path.join(temp_dir, pdf.name), pdf)
return temp_dir
default_storage(DefaultStorageオブジェクトクラス)はDjango標準で備わっているファイルオブジェクトを操作するためのメソッドで、様々なファイル操作を行うことができます。
default_storageを利用するには以下コードでdefault_storageをインポートする必要があります。
python1 Copied!from django.core.files.storage import default_storage
以下の形式で指定すると指定したファイルを保存先のパスに保存してくれます。
Copied!default_storage.save(保存先のパス, 保存するファイル)
最後にアップロード先のパス情報(temp_dir)を戻り値として返しています。
パス情報(temp_dir)は、後半で実装するPDFからテキストデータを抽出する際に必要となるため、ファイル保存完了時の戻り値として取得しておきます。
続いてアプリケーション直下のurls.pyにファイルアップロード画面用のURLの設定を1つ追加します。
URLパターンの定義は以下の通りです。
Copied!URLパターン名称:upload
URLパス:http://127.0.0.1:8000/upload
呼び出す関数:UploadView.as_view()
追加するコードは以下の1行です。
python1 Copied!path('upload/', views.UploadView.as_view(), name='upload'), #新規追加
python12345678910111213 Copied!../tutorial
├── accounts
├── pdfmr
| └── forms.py
| ├── admin.py
| ├── apps.py
| ├── models.py
| ├── tests.py
| ├── urls.py #変更対象
| ├── views.py
├── templates
├── manage.py
└── tutorial
変更前
python123456789 Copied!from django.contrib import admin
from django.urls import path
from . import views
app_name ="pdfmr"
urlpatterns = [
path('top/', views.top, name='top'),
]
変更後
python12345678910 Copied!from django.contrib import admin
from django.urls import path
from . import views
app_name ="pdfmr"
urlpatterns = [
path('top/', views.top, name='top'),
path('upload/', views.UploadView.as_view(), name='upload'), #新規追加
]
現時点でviews.pyにUploadViewクラスは存在していないためエラーになりますが無視してください。
以上で、このパートは完了です。
お疲れさまでした。