【Python】続・PDFの文章をページ毎にCSVに変換

[前回](https://qiita.com/wagahaiCAT/items/63489b6ec6c0c3559886)のあと、コレ改修が必要だなと思ったので地味な続きです。

#事の発端
PDFのページをCSV出力したはいいけれど、トンデモデータになり申した。
具体的に言うと、サブタイトルが真ん中に来たりしてました。地味につらい。

なかなか類似案件が見つからずもだもだしてたところ、以下のサイトを発見。
[厚生労働省のブラック企業リストをPythonで解析する（PDFMiner.six）](https://a244.hateblo.jp/entry/2017/08/26/001050)

同志がいたことと、座標で何とかできそうなのはわかりました。
のでやってみようと思います。


##検証-準備-
参考：[PDFから文字情報を抽出するには、PDFMiner一択](https://qiita.com/fumitrial8/items/f3d92fca0de409feaee9)

pdfminerではレイアウトの座標情報も取れるらしいです。
今まではTextConverterで文字データだけを抜いていましたが、
PDFPageAggregatorでは座標と文字データが引っこ抜けるようなのでこちらを使います。

とりあえず、どんな座標とれてるかチェックします。
サンプルPDF用意できてなくて申し訳ない…。

```python
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.converter import TextConverter, PDFPageAggregator
from pdfminer.layout import LAParams, LTContainer, LTTextBox, LTTextLine, LTChar
from pdfminer.pdfpage import PDFPage

def convert_pdf_to_txt(self,p_d_f):
    
    fp = open(p_d_f, 'rb')
    for page in PDFPage.get_pages(fp):
        
        rsrcmgr = PDFResourceManager()
        laparams = LAParams()
        laparams.detect_vertical = True
        device = PDFPageAggregator(rsrcmgr, laparams=laparams)
        interpreter = PDFPageInterpreter(rsrcmgr, device)
        
        #PDFから座標と文字データを取得
        interpreter.process_page(page)
        layout = device.get_result()
        
        #座標と文字の表示
        for node in layout:
            if isinstance(node, LTTextBox) or isinstance(node, LTTextLine):
                print(node.get_text())   #文字
                word =input(node.bbox)   #座標
        word =input("---page end---")
```

コマンドプロンプトでぽちぽちチェックする非効率なやつ。

正直LTTextBoxみたいな判定よくわかってないですが、おまじない的に入れてしまっている。
ちゃんと調べよう。

##検証-結果-
出力結果の抜粋です。
文章はダミーです。

```
---page end---
ポップコーン機について

(68.28, 765.90036, 337.2, 779.9403599999999)
ぱちぱちはじけてポップコーンを作る機械です。

(67.8, 697.71564, 410.4000000000001, 718.47564)
使うときは安全に気を付けてください。

(67.8, 665.29564, 339.8400000000002, 686.05564)
使い方は以下の通り。

(67.8, 643.69564, 279.3600000000001, 653.65564000)
説明

(67.8, 730.11564, 87.96000000000001, 740.07564)
```

タプルが座標です。順番は(x0,y0,x1,y1)。詳しくは参考サイトへGO！
端的に言うと、y1を見れば一番下からの文字の座標がわかります。
つまり、ページ内のy1が降順であれば上から順番の座標に文字が配置されている＝正しい配置形態、です（今回の場合は）。

で、この出力結果を見ると最後の行のy1が二番目に大きいので、単純に上から並べるという観点では的外れな結果です。
x0を基準に並び替えられている可能性があります。嘘です何もわかりません。
座標はうまくとれてるっぽいので、このy1を使ってなんとかすることにします。


#解決方法案
①辞書つくる
②辞書をソート（キー降順）
③それを文字列にする
④改行を一掃する

これでうまくいくはず。
しゃらくせえ人は完成品だけ見てってください。


##①辞書つくる

```python
d=[]
for node in layout:
    if isinstance(node, LTTextBox) or isinstance(node, LTTextLine):
        y1 = node.bbox[3]
        #表ならy1の座標が重複するので文字列結合
        if y1 in d:
           d[y1] += "|" + node.get_text()
        else:
           d[y1] = node.get_text()
```
座標と文字の辞書をさくさく作ります。気休め程度に表対策もします。

ですが、正直に言うとこの表ぽくする方法は穴があるので不毛な努力です。
というのも、先ほどの座標って一行ずついい感じに文字を取っているように見えるのですが、仕組みとしてはマージンパディング的な値を設定して、近々の文字の塊を「ブロック」として取っているらしいのです（たしか）。

ベタな話、何も設定しなければデフォルトのマージンが適用されて、行間つめつめの文章とか細かめの表なんかは複数行を一つのブロックと認識されます。
なので、同じ座標として複数行の文字がドカッと取得されたら、もうエセ表作戦崩壊です。

ならマージンパディング的なのをちゃんと設定しろって話ですけども、今回はそこまで求めてないので特に設定はしません。
表が出たら、「残念だったな！」程度の気持ちでレッツトライ。



##②辞書のソート（キー降順）
参考：[Python sortのまとめ (リスト、辞書型、Series、DataFrame)](https://qiita.com/shizuma/items/40f1fe4702608db40ac3)

```python
d2 = sorted(d.items(), key=lambda x: -x[0])
```

やったぜ！　らむだはつよし！
ちなみにこれを実行すると辞書がリストになります。
ソートさえできればいいのであまり気にしません。


##③それを文字列にする

```python
text = ""
for d0 in d2:
     text += d0[1]
```
ただぐるぐるしてるだけ。


##④改行を一掃する
参考：[Python, splitでカンマ区切り文字列を分割、空白を削除しリスト化](https://note.nkmk.me/python-split-strip-list-join/)
いつもお世話になってます。

```python
space = re.compile("[ 　]+")
text = re.sub(space, "", text )
l_text = [a for a in text.splitlines() if a != '']
text = '\n'.join(l_text).replace('\n|', '|')
```
空白と改行が多いよ問題の解決案です。
空白は置換、改行はリストにして抹消します。
ついでに表もどきの時に目印にしてた記号の前にある改行も削除します。


#完成品

```python

from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.converter import TextConverter, PDFPageAggregator
from pdfminer.layout import LAParams, LTContainer, LTTextBox, LTTextLine, LTChar
from pdfminer.pdfpage import PDFPage

import csv,re,datetime
import pandas as pd

class converter(object):
    def convert_pdf_to_txt(self,p_d_f):
        print("system：pdf【" + p_d_f + "】を読込みます")
        
        df = pd.DataFrame(columns=["更新日時","文章","ページ番号"])
        
        cnt = 1
        space = re.compile("[ 　]+")
        fp = open(p_d_f, 'rb')
       
        #pdfから座標と文字データ引っこ抜く
        for page in PDFPage.get_pages(fp):
            rsrcmgr = PDFResourceManager()
            laparams = LAParams()
            laparams.detect_vertical = True
            device = PDFPageAggregator(rsrcmgr, laparams=laparams)
            interpreter = PDFPageInterpreter(rsrcmgr, device)
            #PDFから座標と文字データを取得
            interpreter.process_page(page)
            layout = device.get_result() 
            
            #座標とデータの辞書をつくる
            d={}
            for node in layout:
                if isinstance(node, LTTextBox) or isinstance(node, LTTextLine):
                    y1 = node.bbox[3]
                    #表ならy1の座標が重複するので文字列結合
                    if y1 in d:
                       d[y1] += "|" + node.get_text()
                    else:
                       d.update({y1 : node.get_text()})
            
            #座標順にソート
            d2 = sorted(d.items(), key=lambda x: -x[0])
            
            #文字列にぶっこむ
            text = ""
            for d0 in d2:
                 text += ddd[1]
            
            #空白改行の削除   
            text = re.sub(space, "", text)
            l_text = [a for a in text.splitlines() if a != '']
            text = '\n'.join(l_text).replace('\n|', '|')     
            
            df.loc[cnt,["文章","ページ番号"]] = [text,cnt]
            cnt += 1
            
        fp.close()
        device.close()
         
        now = datetime.datetime.now()
        df["更新日時"] = now

        csv_path = p_d_f.replace('.pdf', '.csv')
        with open(csv_path, mode='w', encoding='cp932', errors='ignore', newline='\n') as f:
             df.to_csv(f,index=False)

if __name__ == "__main__":

  p_d_f = "なんちゃか.pdf"
  con=converter()
  hoge=con.pdf_to_csv(p_d_f)
```
前回から足し算引き算したのでよく確認してないですが、似たようなものは動きましたメモ。
エラーでたら都度都度自前で直してくださひ。