pdfminerでテキストを座標で抽出しCSVに保存

PDFスクレイピングよりテキストのみだったので座標とページ数を追加しCSVに保存できるように変更

良いところ

  • Python
  • 縦書きも取り込める

不便なところ

  • 座標がfloatで細かい
  • 行が違う場合もまとめて取ってしまう。line_marginで調整できそう。
  • 取得できないテキストもある。pdf2txt.pyだと取れるので設定しだいかも。

インストール

pip install pdfminer

ソース

import sys
import csv

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

# 再帰的にテキストボックス(LTTextBox)を探して、テキストボックスのリストを取得する。
def find_textboxes_recursively(layout_obj):

    # LTTextBoxを継承するオブジェクトの場合は1要素のリストを返す。
    if isinstance(layout_obj, LTTextBox):
        return [layout_obj]

    # LTContainerを継承するオブジェクトは子要素を含むので、再帰的に探す。
    if isinstance(layout_obj, LTContainer):
        boxes = []
        for child in layout_obj:
            boxes.extend(find_textboxes_recursively(child))

        return boxes

    # その他の場合は空リストを返す。
    return []


# Layout Analysisのパラメーターを設定。縦書きの検出を有効にする。
laparams = LAParams(detect_vertical=True)

# 共有のリソースを管理するリソースマネージャーを作成。
resource_manager = PDFResourceManager()

# ページを集めるPageAggregatorオブジェクトを作成。
device = PDFPageAggregator(resource_manager, laparams=laparams)

# Interpreterオブジェクトを作成。
interpreter = PDFPageInterpreter(resource_manager, device)

csv_data = []

# ファイルをバイナリ形式で開く。
with open(sys.argv[1], "rb") as f:

    # PDFPage.get_pages()にファイルオブジェクトを指定して、PDFPageオブジェクトを順に取得する。
    # 時間がかかるファイルは、キーワード引数pagenosで処理するページ番号(0始まり)のリストを指定するとよい。
    for i, page in enumerate(PDFPage.get_pages(f), start=1):

        # ページを処理する。
        interpreter.process_page(page)

        # LTPageオブジェクトを取得。
        layout = device.get_result()

        # ページ内のテキストボックスのリストを取得する。
        boxes = find_textboxes_recursively(layout)

        # テキストボックスの左上の座標の順でテキストボックスをソートする。
        # y1(Y座標の値)は上に行くほど大きくなるので、正負を反転させている。
        boxes.sort(key=lambda b: (-b.y1, b.x0))

        for box in boxes:
            # ページ数、X座標、Y座標、テキスト
            csv_data.append([i, box.x0, box.y1, box.get_text().strip()])

with open("data.csv", "wt") as fw:
    writer = csv.writer(fw, lineterminator="\n")
    writer.writerows(csv_data)

ランナー分析

import pandas as pd
import matplotlib.pyplot as plt

import japanize_matplotlib

# 解像度
import matplotlib as mpl
mpl.rcParams['figure.dpi'] = 200

dfs = pd.read_html("https://www.pref.ehime.jp/h14150/malaysiabadminton/seika_runner.html")

df = pd.concat([dfs[0], dfs[2]])

df.drop("性別.1", axis=1, inplace=True)

df["年齢"] = df["年齢"].str.strip("()").astype(int)

df.reset_index(drop=True, inplace=True)

df.loc[df["性別"] == "男", "男"] = df["年齢"].astype(int)
df.loc[df["性別"] == "女", "女"] = df["年齢"].astype(int)

df.info()

df["年齢"].describe()

"""
count    43.000000
mean     34.767442
std      22.761535
min      12.000000
25%      15.000000
50%      27.000000
75%      53.500000
max      90.000000
Name: 年齢, dtype: float64
"""

df.groupby(by=["性別"])["年齢"].describe()
ax = df[df["性別"]=="男"]["年齢"].hist(bins=range(10, 100, 5), alpha=0.6)
ax = df[df["性別"]=="女"]["年齢"].hist(bins=range(10, 100, 5), alpha=0.6, ax=ax)
ax.set_xticks(range(0,100,10))

# グラフを保存
plt.savefig('01.png', dpi=200, bbox_inches="tight")
plt.show()

f:id:imabari_ehime:20191219193528p:plain

df_sort = df.sort_values(by="年齢", inplace=True)

ax = df.plot.barh(x="公募・推薦市町",y=["男","女"], figsize=(5, 10), )
# 垂直
ax.set_xticks(range(0,100,10))
ax.axvline(x=34.77, linestyle="--", color="orange", linewidth = 1, label="")
ax.axvline(x=22.76, linestyle="--", color="green", linewidth = 1, label="")
# グラフを保存
plt.savefig('02.png', dpi=200, bbox_inches="tight")
plt.show()

f:id:imabari_ehime:20191219193536p:plain

ax = df_mean.plot.barh()

# 垂直
ax.axvline(x=34.77, linestyle="--", color="orange", linewidth = 1, label="")
ax.axvline(x=22.76, linestyle="--", color="green", linewidth = 1, label="")
# グラフを保存
plt.savefig('03.png', dpi=200, bbox_inches="tight")
plt.show()

f:id:imabari_ehime:20191219193545p:plain

JFLランキング作成

imabari.hateblo.jp

新たに作成

  • crosstabに変更
  • lambdaに変更
import pandas as pd
import numpy as np

# 行数
pd.set_option("display.max_columns", None)

# データ取得

# 試合結果を取得
url = "http://www.jfl.or.jp/jfl-pc/view/s.php?a=1411&f=2019A001_spc.html"
dfs = pd.read_html(url, na_values="-")

# 前処理

# 列名変更
for i in range(len(dfs)):
    dfs[i].columns = ["日にち", "時間", "ホーム", "スコア", "アウェイ", "スタジアム", "備考"]

## 結合

# 結合、節を追加
df = pd.concat(dfs, keys=[i for i in range(1, len(dfs) + 1)], names=["節", "番号"])

# 備考削除
df.drop("備考", axis=1, inplace=True)

# 内容確認
df.head()

## 欠損削除

# 欠損値確認
df[df.isnull().any(axis=1)]

# スコアがないものを除去
df.dropna(subset=["スコア"], inplace=True)

## スコア分割

# スコアを分割、スコアを削除、結合
df1 = pd.concat([df, df["スコア"].str.split("-", expand=True)], axis=1).drop("スコア", axis=1)

# 名前をホーム得点、アウェイ得点に変更
df1.rename(columns={0: "ホーム得点", 1: "アウェイ得点"}, inplace=True)

# ホーム得点、アウェイ得点を文字から整数に変更
df1["ホーム得点"] = df1["ホーム得点"].astype(int)
df1["アウェイ得点"] = df1["アウェイ得点"].astype(int)
df1.dtypes

# データ確認
df1.head()

## ホーム・アウェイ別集計

# ホームの結果のみ
df_home = df1.loc[:, ["ホーム", "ホーム得点", "アウェイ得点"]]
df_home.columns = ["チーム名", "得点", "失点"]
df_home["戦"] = "ホーム"
df_home.head()

# アウェイの結果のみ
df_away = df1.loc[:, ["アウェイ", "アウェイ得点", "ホーム得点"]]
df_away.columns = ["チーム名", "得点", "失点"]
df_away["戦"] = "アウェイ"
df_away.head()

# ホームとアウェイを結合
df_total = pd.concat([df_home, df_away])

## 得失点計算

# 得失点を計算
df_total["得失点"] = df_total["得点"] - df_total["失点"]

## 勝敗・勝点追加

# 勝敗追加
df_total["勝敗"] = df_total["得失点"].apply(lambda x: "引分" if x == 0 else "敗戦" if x < 0 else "勝利")

# 勝点追加
df_total["勝点"] = df_total["得失点"].apply(lambda x: 1 if x == 0 else 0 if x < 0 else 3)

df_total.head()

# 集計

## 得失点・勝敗・勝点集計


# 得点・失点・得失点・勝点 集計
pv_score = df_total.pivot_table(values=["得点", "失点", "得失点", "勝点"], index="チーム名", aggfunc=sum)

pv_score

## 勝敗カウント

# 勝敗集計
pv_wl = pd.crosstab(df_total["チーム名"], [df_total["戦"], df_total["勝敗"]])

# 列名変更
pv_wl.columns = ["勝利A", "引分A", "敗戦A", "勝利H", "引分H", "敗戦H"]

# 合計追加
pv_wl["勝利"] = pv_wl["勝利H"] + pv_wl["勝利A"]
pv_wl["引分"] = pv_wl["引分H"] + pv_wl["引分A"]
pv_wl["敗戦"] = pv_wl["敗戦H"] + pv_wl["敗戦A"]

# 試合数追加
pv_wl["試合数"] = pv_wl["勝利"] + pv_wl["引分"] + pv_wl["敗戦"]

# 確認
pv_wl

## 評価値

df2 = df_total.copy()

# 評価値を作成
df2["評価値"] = ((df2["勝点"]) * 10000) + (df2["得失点"] * 100) + df2["得点"]
df2

# 評価値集計
df3 = df2.pivot_table(values="評価値", index="チーム名", columns="節", aggfunc=sum, fill_value=0)
df3

# 評価値の累計和
df_eval = df3.apply(lambda d: d.cumsum(), axis=1)
df_eval

## 順位

# 評価値から順位作成
df_chart = df_eval.rank(ascending=False, method="min").astype(int)
df_chart.sort_values(by=df_chart.columns[-1], inplace=True)
df_chart

# 最終順位取得
s1 = df_chart.iloc[:, -1]
s1.name = "順位"

## 順位差分

df_diff = df_chart.diff(axis=1).fillna(0).astype(int)
df_diff

#  前節差分
s2 = df_diff.iloc[:, -1].apply(lambda x: "-" if x == 0 else "▼" if x < 0 else "▲")
s2.name = "前節"
s2

# ランキング作成

# 全部を結合
df4 = pd.concat([pv_score, pv_wl], axis=1).join([s1, s2])

# 順位で昇順
df4.sort_values(["順位"], inplace=True)

df4

# ランキング完成

df_rank = df4.reset_index().loc[:, ["前節", "順位", "チーム名", "勝点", "試合数", "勝利", "勝利H", "勝利A", "引分", "引分H", "引分A", "敗戦", "敗戦H", "敗戦A", "得失点", "得点", "失点"]]

df_rank

名古屋市のデータ解析練習

oku.edu.mie-u.ac.jp

twilog.org

Pandasで試してみたけど後半はやり方がわからない

こんなのができるようになりたい

import pandas as pd
import matplotlib.pyplot as plt
import japanize_matplotlib

df = pd.read_csv("Nagoya-HPVV-data.csv", index_col=0, header=None, dtype="object")

df.columns += 1

df = df.rename(columns=lambda x: str(x))

len(df)

df.head()
# 子宮頸がん予防接種(HPVV)の有無

df["233"].value_counts().sort_index()
# 例:「身体が自分の意思に反して動く」

df["145"].value_counts().sort_index()
# HPVV有無とクロス集計

df0 = pd.crosstab(df["233"], df["145"])
df0
# Fisherの正確検定

import scipy.stats as stats
import numpy as np

oddsratio, pvalue = stats.fisher_exact(df0.iloc[1:3, 1:3].values)

print(f"p-value = {pvalue}")
print(f"odds ratio = {oddsratio}")
# 生まれた年度とHPVV接種率

# 生まれた年度の分布
df1 = df.loc[:, "5":"11"].copy().astype("int64")
df1.columns = range(6, 13)

birth = df1[df1.sum(axis=1) == 1].idxmax(axis=1)
birth = birth.reindex(df1.index)

birth.value_counts(dropna=False).sort_index()

# 生まれた年度とHPVV接種率
df2 = pd.crosstab(df["233"], birth)
df2

df2t = df2.iloc[1:3, :].T
s1 = df2t["2"] / (df2t["1"] + df2t["2"]) * 100

ax = s1.plot(xlim=(5.5, 12.5), ylim=(0, 100), marker="o")
ax.set_xlabel("生まれた年度(平成)")
ax.set_ylabel("接種率(%)")
plt.show()
# 生まれた年度と「身体が自分の意思に反して動く」のクロス集計
pd.crosstab(df["145"], birth)

子宮頸がん予防接種調査の結果のPDFをCSV化

www.city.nagoya.jp

oku.edu.mie-u.ac.jp

togetter.com

PDFをXMLに変換しTOP・LEFTで並び替えして抽出する

完成したCSVファイル

drive.google.com

Gist

github.com

PDF変換ソフトをインストール・ダウンロード

!apt install poppler-utils

!wget https://www-eu.apache.org/dist/pdfbox/2.0.17/pdfbox-app-2.0.17.jar -O pdfbox-app.jar

PDFをXMLに変換 ※目的のファイルを実行

# kaitodeta1.pdf
!wget http://www.city.nagoya.jp/kenkofukushi/cmsfiles/contents/0000088/88972/kaitodeta1.pdf -O data.pdf

!java -jar pdfbox-app.jar PDFSplit -split 155 data.pdf

!for i in {1..16}; do pdftohtml -q -xml data-${i}.pdf data-${i}.xml; done

n = 17
# kaitodeta2.pdf
!wget http://www.city.nagoya.jp/kenkofukushi/cmsfiles/contents/0000088/88972/kaitodeta2.pdf -O data.pdf

!java -jar pdfbox-app.jar PDFSplit -split 155 data.pdf

!for i in {1..16}; do pdftohtml -q -xml data-${i}.pdf data-${i}.xml; done

n = 17
# kaitodeta3.pdf
!wget http://www.city.nagoya.jp/kenkofukushi/cmsfiles/contents/0000088/88972/kaitodeta3.pdf -O data.pdf

!java -jar pdfbox-app.jar PDFSplit -split 155 data.pdf

!for i in {1..25}; do pdftohtml -q -xml data-${i}.pdf data-${i}.xml; done

n = 26
# kaitodeta4.pdf
!wget http://www.city.nagoya.jp/kenkofukushi/cmsfiles/contents/0000088/88972/kaitodeta4.pdf -O data.pdf

!java -jar pdfbox-app.jar PDFSplit -split 155 data.pdf

!for i in {1..17}; do pdftohtml -q -xml data-${i}.pdf data-${i}.xml; done

n = 18
# kaitodeta5.pdf
!wget http://www.city.nagoya.jp/kenkofukushi/cmsfiles/contents/0000088/88972/kaitodeta5.pdf -O data.pdf

!java -jar pdfbox-app.jar PDFSplit -split 175 data.pdf

!for i in {1..16}; do pdftohtml -q -xml data-${i}.pdf data-${i}.xml; done

n = 17

XMLからCSVに変換

from xml.etree import ElementTree as ET
import csv

result = []

for i in range(1, n):

    tree = ET.parse(f"data-{i}.xml")
    root = tree.getroot()

    pages = root.findall("page")

    for page in pages:

        # print(page.attrib)

        for item in page.findall("text"):

            temp = item.attrib
            temp["side"] = i
            temp["text"] = item.text.strip()
            temp["page"] = page.get("number")

            if temp["text"]:
                result.append(temp)
            else:
                print(item.text)

with open("data.csv", "w", newline="", encoding="utf-8") as fw:

    fieldnames = ["page", "side", "top", "left", "width", "height", "font", "text"]

    writer = csv.DictWriter(fw, fieldnames=fieldnames)

    writer.writeheader()

    for row in result:
        writer.writerow(row)

データ調整

import pandas as pd

df1 = pd.read_csv("data.csv")

# 1ページ目のヘッダー部分を削除
df2 = df1.drop(df1[(df1["page"] == 1) & (df1["top"] < 510)].index)

# ページ・サイド・縦・横・テキスト抽出
df3 = df2.loc[:, ["page", "side", "top", "left", "text"]]

df3

# X座標でカウント
df_count = df3.pivot_table(index=["side", "left"], aggfunc="count")

## ずれを確認
df_count
df_count.to_csv("count.tsv", sep="\t")

ずれ調整 ※目的のファイルを実行

# kaitodeta1.pdf
df3.loc[(df3["side"] == 8) & (df3["left"] == 383), "left"] = 382
df3.loc[(df3["side"] == 8) & (df3["left"] == 765), "left"] = 766
df3.loc[(df3["side"] == 12) & (df3["left"] == 646), "left"] = 647
df3.loc[(df3["side"] == 12) & (df3["left"] == 882), "left"] = 883
df3.loc[(df3["side"] == 14) & (df3["left"] == 648), "left"] = 647
df3.loc[(df3["side"] == 14) & (df3["left"] == 680), "left"] = 681

# 220列に空列生成
s1 = pd.Series([1, 10, 522, 600, ""], index=df3.columns, name=9999998)
df3 = df3.append(s1)

# 223列に空列生成
s2 = pd.Series([1, 10, 522, 800, ""], index=df3.columns, name=9999999)
df3 = df3.append(s2)
# kaitodeta2.pdf
df3.loc[(df3["side"] == 9) & (df3["left"] == 383), "left"] = 382
df3.loc[(df3["side"] == 12) & (df3["left"] == 356), "left"] = 355
df3.loc[(df3["side"] == 12) & (df3["left"] == 857), "left"] = 856
df3.loc[(df3["side"] == 12) & (df3["left"] == 1125), "left"] = 1126
df3.loc[(df3["side"] == 13) & (df3["left"] == 560), "left"] = 559
df3.loc[(df3["side"] == 13) & (df3["left"] == 592), "left"] = 593

# 220列に空列生成
s1 = pd.Series([1, 9, 519, 900, ""], index=df3.columns, name=9999999)
df3 = df3.append(s1)
# kaitodeta3.pdf
df3.loc[(df3["side"] == 24) & (df3["left"] == 739), "left"] = 740
df3.loc[(df3["side"] == 24) & (df3["left"] == 852), "left"] = 851
df3.loc[(df3["side"] == 24) & (df3["left"] == 884), "left"] = 885
# kaitodeta4.pdf
df3.loc[(df3["side"] == 6) & (df3["left"] == 682), "left"] = 683
df3.loc[(df3["side"] == 15) & (df3["left"] == 651), "left"] = 652
# kaitodeta5.pdf
df3.loc[(df3["side"] == 8) & (df3["left"] == 767), "left"] = 768
df3.loc[(df3["side"] == 11) & (df3["left"] == 356), "left"] = 355
df3.loc[(df3["side"] == 13) & (df3["left"] == 1027), "left"] = 1026
df3.loc[(df3["side"] == 13) & (df3["left"] == 1060), "left"] = 1059
df3.loc[(df3["side"] == 15) & (df3["left"] == 470), "left"] = 469
df3.loc[(df3["side"] == 15) & (df3["left"] == 502), "left"] = 503

TSVに変換

# TOP、LEFTで並び替え
df = df3.pivot_table(
    index=["page", "top"],
    columns=["side", "left"],
    values="text",
    aggfunc=lambda x: " ".join(str(v) for v in x),
)

# ファイルに書き出し
df.to_csv("result.tsv", sep="\t", index=False, header=False)

TSV調整

import re

with open("result.tsv") as fr:
    tsv_text = fr.read()

# 数字 半角スペース 数字の場合、数字 タブ 数字に置換
pattern1 = re.compile(r"(?<=\d) (?=\d)")
tsv_text = pattern1.sub("\t", tsv_text)

# 半角スペース 数字 タブ タブの場合、タブ 数字 タブに置換
pattern2 = re.compile(r" (\d)\t\t")
tsv_text = pattern2.sub(r"\t\1\t", tsv_text)

# kaitodeta1.pdfのみ実行 ※1
# tsv_text = re.sub(r"0\t\[04\] 月経量の異常による低血圧\t0\t\t00", r"0\t[04]\t月経量の異常による低血圧\t0\t00", tsv_text)

with open(f"result-{pdf_num}.tsv", mode="w") as fw:
    fw.write(tsv_text)

TSVファイルを結合

dfs = []

for i in range(1, 6):
    dfs.append(pd.read_csv(f"result-{i}.tsv", delimiter='\t', header=None, dtype=object))

df4 = pd.concat(dfs)

# 275質5_2中止理由その他を結合
df4[274] = df4[274].fillna("") + df4[275].fillna("") + df4[276].fillna("") + df4[277].fillna("")

# 275質5_2中止理由その他を不要部分を削除
df4.drop(columns=[275, 276, 277], inplace=True)

# 最終結果をファイルに書き出し
df4.to_csv('result_all.csv', index=False, header=False)

追記

kaitodeta1.pdf

  • 行2920列219を分解し220、221にそれぞれ入力 ※1

kaitodeta1.pdfとkaitodeta2.pdf

  • 列275の結合は3箇所境界部分の文字が重なっているので上記のcsvでは削除しています。

空き室状況をCSVに変換

シクロの家の空き室状況がわかりにくかったのでCSVに変換

import calendar
import datetime
import re

import pandas as pd
import requests
from bs4 import BeautifulSoup

# スクレイピング
def scraping():

    url = "http://www.cyclonoie.com/availability.php"

    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko"
    }

    r = requests.get(url, headers=headers)

    r.raise_for_status()

    soup = BeautifulSoup(r.content, "html5lib")

    result = []

    for i in soup.find("div", class_="alpha-web", style="text-align: center;").select(
        "p.alpha-web-text01"
    ):

        text = i.get_text(strip=True)
        pattern = r"\d{1,2}月\d{1,2}日"

        matchOB = re.match(pattern, text)

        if matchOB:
            t = [j.strip() for j in re.split("(ミックスドミトリー|女性ドミトリー|2人用個室|4人用個室)", text)]
            result.append(t)

        return result


# 月日から年月日に変換(うるう年用)
def dt_conv(s):

    today = datetime.date.today()

    try:

        dt = datetime.datetime.strptime(s, "%m月%d日")
        dt = dt.replace(year=today.year).date()

    except:

        year = today.year

        while not calendar.isleap(year):

            year += 1

        m = re.match(r"(\d{1,2})月(\d{1,2})日", s)

        month, day = map(int, m.groups())

        dt = datetime.date(year, month, day)

    return dt


if __name__ == "__main__":

    # スクレイピング
    result = scraping()

    # DataFrameに変換
    df = pd.DataFrame(result)

    # 日付とイベント分離
    df1 = df[0].str.extract(r"(\d{1,2}月\d{1,2}日)(.)(イベントのため(休館|貸切))?")
    df1.rename(columns={0: "日付", 1: "イベント", 2: "状況"}, inplace=True)

    # 結合
    df2 = pd.concat([df, df1], axis=1)

    # 部屋名除去
    df3 = df2.drop(df2.columns[[0, 1, 3, 5, 7, 10]], axis=1)

    # 月日から年月日に変換
    df3["日付"] = df3["日付"].apply(dt_conv)

    # 日付をインデックスに設定
    df3.set_index("日付", inplace=True)

    # 列名変更
    df3.rename(
        columns={2: "ミックスドミトリー", 4: "女性ドミトリー", 6: "2人用個室", 8: "4人用個室"}, inplace=True
    )

    # 残り数を数字のみに変更
    df3.replace(r"\s", "", regex=True, inplace=True)
    df3.replace("○Last", "", regex=True, inplace=True)

    # CSVに書き出し
    df3.to_csv("result.csv", encoding="utf_8_sig")

うるう年の"2月29日"の文字列をdatetimeに変換

月日だけの"2月29日"の文字列からdatetimeに変換すると1900-02-29になり うるう年ではないためエラーが発生

直近のうるう年まで進める

import calendar
import datetime
import re

s = "2月29日"

today = datetime.date.today()

try:

    dt = datetime.datetime.strptime(s, "%m月%d日")
    dt = dt.replace(year=today.year).date()

except:

    year = today.year

    while not calendar.isleap(year):

        year += 1

    m = re.match(r"(\d{1,2})月(\d{1,2})日", s)

    month, day = map(int, m.groups())

    dt = datetime.date(year, month, day)

print(dt)