paloma blog

NWエンジニアやってます。主に自宅環境のお遊びを書きます。Pythonもちょっと。タイトルは好きなカクテルから。

pythonのscrapyを使って小鉢料理マスターを目指す

小鉢料理っていいですよね。 少量ではありますがいろんなものをたくさん食べられます。

私はお酒が好きで店に限らず家でもよく飲みます。
おつまみも自分で作るのですがどうしても焼き物、揚げ物メインであとは豆腐やチーズといった出来合いを出して終わってしまいます。

家での飲みライフを充実させるべく小鉢料理のレパートリーを増やしたいと思い、レシピをかき集めることにしました。

スクレイピングで収集

レシピといえば料理本を買うのもいいですが今の時代はインターネットに山ほど情報が転がっています。
そして私はITのエンジニア。
スクレイピングで収集しない手はありません。

※ウェブサイトをスクレイピングする前に、必ず利用規約を読みrobots.txtガイドラインに従ってください。

scrapyライブラリ

私はpythonを使いますがスクレイピングといえばBeautifulSoupが有名です。
しかし今回はスクレイピングで検索すると何かと出てくるscrapyライブラリを使ってみたいと思います。
スクレイピングに限らずクローラーとしての機能もあるようです。

Scrapy | A Fast and Powerful Scraping and Web Crawling Framework

環境

いつものubuntu
pythonは仮想環境内です。

(python3) masashi@PC-ubuntu:~$ uname -a ; lsb_release -a
Linux PC-ubuntu 5.4.0-89-generic #100-Ubuntu SMP Fri Sep 24 14:50:10 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.3 LTS
Release:    20.04
Codename:   focal
(python3) masashi@PC-ubuntu:~$ python --version
Python 3.8.10

インストール

いくつか方法はありますがpipでインストールします。

(python3) masashi@PC-ubuntu:~$ pip install scrapy

バージョン。

(python3) masashi@PC-ubuntu:~$ scrapy version
Scrapy 2.5.1

収集するサイト

小鉢料理のレシピサイトも多数ありますが、今回はホットペッパーのメシ通というサイトを収集したいと思います。
このサイトのレシピは小鉢に限っていませんが、お手軽そうで記事の内容も面白かったので。

レシピ カテゴリーの記事一覧 - メシ通 | ホットペッパーグルメ

コード

取得対象

本サイトに限らずですが、レシピの一覧から詳細ページに飛ぶという作りになっています。
詳細は都度飛べばいいと思うのでとりあえずタイトルとリンクだけ収集したいと思います。

プロジェクト作成

最初にスクレイピング用のプロジェクトを作成します。

(python3) masashi@PC-ubuntu:~$ cd scrapy
(python3) masashi@PC-ubuntu:~/scrapy$ scrapy startproject sidedish
New Scrapy project 'sidedish', using template directory '/home/masashi/python3/lib/python3.8/site-packages/scrapy/templates/project', created in:
    /home/masashi/scrapy/sidedish

You can start your first spider with:
    cd sidedish
    scrapy genspider example example.com

ツリー構成。

(python3) masashi@PC-ubuntu:~/scrapy$ cd sidedish
(python3) masashi@PC-ubuntu:~/scrapy/sidedish$ tree
.
├── scrapy.cfg
└── sidedish
    ├── __init__.py
    ├── items.py
    ├── middlewares.py
    ├── pipelines.py
    ├── settings.py
    └── spiders
        └── __init__.py

2 directories, 7 files
スクレイピング用コード

チュートリアルに沿って作成。
スクレイピング用クラスを作成してコマンドラインから実行という流れです。
タグの抽出はcssを使うサンプルが多かったので私もそうしました。
BeautifulSoupではクラス名とかxpathをよく使っていたのですが、cssはツリーがわかりやすくていいですね。

import scrapy

class SidedishSpider(scrapy.Spider):
    name = 'sidedish'

    start_urls = [
        'https://www.hotpepper.jp/mesitsu/archive/category/%E3%83%AC%E3%82%B7%E3%83%94'
    ]

    def parse(self, response):
        for dish in response.css('section div h1'):
            yield {
                'text': dish.css('a::text').get(),
                'link': dish.css('a').attrib['href'],
            }
実行

実行してみます。

(python3) masashi@PC-ubuntu:~/scrapy/sidedish$ scrapy crawl sidedish
2021-10-31 19:06:14 [scrapy.utils.log] INFO: Scrapy 2.5.1 started (bot: sidedish)
()
2021-10-31 19:06:14 [scrapy.core.engine] INFO: Spider opened
2021-10-31 19:06:14 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2021-10-31 19:06:14 [scrapy.extensions.telnet] INFO: Telnet console listening on 127.0.0.1:6023
2021-10-31 19:06:14 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://www.hotpepper.jp/robots.txt> (referer: None)
2021-10-31 19:06:14 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://www.hotpepper.jp/mesitsu/archive/category/%E3%83%AC%E3%82%B7%E3%83%94> (referer: None)
2021-10-31 19:06:15 [scrapy.core.scraper] DEBUG: Scraped from <200 https://www.hotpepper.jp/mesitsu/archive/category/%E3%83%AC%E3%82%B7%E3%83%94>
{'text': '一皿でタンパク質約26g、糖質控えめオートミールのパラパラチャーハンの作り方。筋トレにハマった管理栄養士のボディメイクめし', 'link': 'https://www.hotpepper.jp/mesitsu/entry/kanakitajima/2021-00455'}
2021-10-31 19:06:15 [scrapy.core.scraper] DEBUG: Scraped from <200 https://www.hotpepper.jp/mesitsu/archive/category/%E3%83%AC%E3%82%B7%E3%83%94>
{'text': '脂がのったサーモン刺身で「サーモングラタン」。骨なし、火の通りの心配なしで最高だった【魚屋三代目】', 'link': 'https://www.hotpepper.jp/mesitsu/entry/sakanaya-sandaime/2021-00454'}
2021-10-31 19:06:15 [scrapy.core.scraper] DEBUG: Scraped from <200 https://www.hotpepper.jp/mesitsu/archive/category/%E3%83%AC%E3%82%B7%E3%83%94>
()
2021-11-01 23:29:49 [scrapy.core.engine] INFO: Spider closed (finished)

いいですね。
取得できてます。

クローリング用コード作成

所謂「次のページへ」があって一覧が分割されてますのでここのリンクを辿ってすべて取得できるようにします。

def perseに追加。下三行です。
「次のページへ」をどんどん辿って最後まで各ページの一覧を取得していきます。

    def parse(self, response):
        for dish in response.css('section div h1'):
            yield {
                'text': dish.css('a::text').get(),
                'link': dish.css('a').attrib['href'],
            }

        next_page = response.css('.test-pager-next').attrib['href']
        if next_page is not None:
            yield response.follow(next_page, callback=self.parse)
保存するため再実行

scrapyは保存のオプションがありますのでこれを使ってデータを保存します。
これもjsonでのサンプルが多かったのでそれに習います。

(python3) masashi@PC-ubuntu:~/scrapy/sidedish$ scrapy crawl sidedish -O sidedishes.json

出力は割愛しますが数十秒ほどで完了。

中身確認

中身を見てみます。

(python3) masashi@PC-ubuntu:~/scrapy/sidedish$ ls -l sidedishes.json 
-rw-rw-r-- 1 masashi masashi 645658 111 23:29 sidedishes.json
(python3) masashi@PC-ubuntu:~/scrapy/sidedish$ wc -l sidedishes.json 
1963 sidedishes.json

1963レシピも収集できました。

(python3) masashi@PC-ubuntu:~/scrapy/sidedish$ head -n2 sidedishes.json 
[
{"text": "\u4e00\u76bf\u3067\u30bf\u30f3\u30d1\u30af\u8cea\u7d0426g\u3001\u7cd6\u8cea\u63a7\u3048\u3081\u30aa\u30fc\u30c8\u30df\u30fc\u30eb\u306e\u30d1\u30e9\u30d1\u30e9\u30c1\u30e3\u30fc\u30cf\u30f3\u306e\u4f5c\u308a\u65b9\u3002\u7b4b\u30c8\u30ec\u306b\u30cf\u30de\u3063\u305f\u7ba1\u7406\u6804\u990a\u58eb\u306e\u30dc\u30c7\u30a3\u30e1\u30a4\u30af\u3081\u3057", "link": "https://www.hotpepper.jp/mesitsu/entry/kanakitajima/2021-00455"},

しかし中身をみるとunicode形式で保存されちゃってます。
これを変換してもいいですが、どうせならそのまま保存したいです。

調べたらsetting.pyに文字コード編集できるところがありました。
encodingの設定を追加。

(python3) masashi@PC-ubuntu:~/scrapy/sidedish$ tail -n1 sidedish/settings.py 
FEED_EXPORT_ENCODING='utf-8'
再実行

何度も仕掛けて申し訳ないですが、再実行。

(python3) masashi@PC-ubuntu:~/scrapy/sidedish$ ls -l sidedishes.json 
-rw-rw-r-- 1 masashi masashi 406541 111 23:37 sidedishes.json

ちょっとサイズが減りましたね。

(python3) masashi@PC-ubuntu:~/scrapy/sidedish$ head -n2 sidedishes.json 
[
{"text": "一皿でタンパク質約26g、糖質控えめオートミールのパラパラチャーハンの作り方。筋トレにハマった管理栄養士のボディメイクめし", "link": "https://www.hotpepper.jp/mesitsu/entry/kanakitajima/2021-00455"},

中身も日本語になってます。 OK!

ちょっとデータ検索

取得ができたということで運用方法はこれから考えますが、ちゃんと取れているか見てみます。
小鉢として私がよく食べる食材でgrepしてみます。

  • 納豆
(python3) masashi@PC-ubuntu:~/scrapy/sidedish$ grep 豆腐 sidedishes.json | head -n3
{"text": " 青唐辛子で作る「白ラー油」は辛みが一味違う→四川料理のスゴい人のレシピを参考に白ラー油の麻婆豆腐も試してみた", "link": "https://www.hotpepper.jp/mesitsu/entry/blues_harp/2021-00608"},
{"text": "タンパク質もうま味も山盛りすぎてローテ入りさせたい「豆腐ステーキ」の作り方【筋肉料理人】", "link": "https://www.hotpepper.jp/mesitsu/entry/kinniku/2021-00453"},
{"text": "「豆腐を入れる」「なすは皮を先に焼く」など、定食屋さんの味に近づくなすみそ炒めレシピのコツ【筋肉料理人】", "link": "https://www.hotpepper.jp/mesitsu/entry/kinniku/2021-00449"},
  • 豆腐
(python3) masashi@PC-ubuntu:~/scrapy/sidedish$ grep 納豆 sidedishes.json | head -n3
{"text": "納豆の練りからし使わない勢は「きゅうりのからし漬け」で消費を。15分くらいで食べごろです【筋肉料理人】", "link": "https://www.hotpepper.jp/mesitsu/entry/kinniku/2021-00357"},
{"text": "納豆を自作して「無限納豆」生活にチャレンジしてみた ", "link": "https://www.hotpepper.jp/mesitsu/entry/oishiisekai/2020-00335"},
{"text": "「納豆、豚ひき肉、春菊の手抜きフィデウア」という材料入れて火をかけるだけのお手軽パスタ料理【ツジメシの付箋レシピ】", "link": "https://www.hotpepper.jp/mesitsu/entry/tsujimeshi/2020-00011"},
  • アボカド
(python3) masashi@PC-ubuntu:~/scrapy/sidedish$ grep アボカド sidedishes.json | head -n3
{"text": " 「アボカド」の食べ方って、もっと可能性があるんじゃないか【やけくそレシピ】", "link": "https://www.hotpepper.jp/mesitsu/entry/chimiwo/2021-00611"},
{"text": "サーモンもお肉もなしで、もれなくウマいアボカド丼ができるレシピ", "link": "https://www.hotpepper.jp/mesitsu/entry/yuuyuu/2021-00422"},
{"text": "電子レンジで完成「にんにく一味肉みそ」で豆腐とアボカドが画力あるつまみに化けた【筋肉料理人】", "link": "https://www.hotpepper.jp/mesitsu/entry/kinniku/2021-00416"},

すべてが小鉢料理ではないですが、レシピデータのサマリとしてはいい感じに収集できたと思います。
このデータをもう一加工すれば小鉢料理のレパートリーとして使えそうですね。
本来は作れてマスターですが、いきなり1000以上のレシピを収集できただけでも近づけているのではないでしょうか。

まとめ

scrapyを使ったクローリング&スクレイピングでした。
フレームワークという名がついているので小難しいお作法を覚えないといけないなと思っていたのですが、
思いの外構文も楽で意外と簡単に実行までできました。

クローリング&スクレイピング自体はBeautifulSoupでも出来るので、scrapyでは他にどんなことが出来るのかも試したいですね。
今回使っていないファイルもありますし、ソースも読めていないのでコマンドから作成クラスをどう呼び出しているかも調べようと思います。

また、スクレイピング単体ではなく過去のツールとかと合わせて使いこなしたいですね。
過去のこれからレシピを引っかけるとか。
HTMLスクレイピングで旬の食材を確認する - paloma blog

参考サイト

Scrapy 2.5 documentation — Scrapy 2.5.1 documentation

How to Crawl the Web with Scrapy | Babbling Fish

Scrapy for Beginners - A Complete How To Example Web Scraping Project - YouTube

SUICAの利用明細をDBに取り込みたい 重複レコード削除編

SUICAの明細をDBに取り込めたのはいいのですが、運用していかないといけません。
SUICAの明細は過去100件分しか出力できないので重複するものが出てきます。

なので重複処理も簡単なDBで管理することにしたのですが、今回2回目のインポートをやってみます。

前回と合わせて2つの明細があります。

  • JE30F819101423311_20210825_20210925110904.pdf
  • JE80F621092507956_20210909_20211013221045.pdf

0825〜0925と0909〜1013の明細なので、09/09から09/25までのレコードが重複しています。

中身チェック

データはインポート済みなので中身を確認します。

件数
sqlite> SELECT count(*) FROM expenses;
200

ファイル2つ分なので200件分入ってます。

重複を除いたレコード数

distinctで重複レコードをまとめることができます。
ネット上ではdistinctでカラムを選択する例が多いですが、*で行ごとまとめられる様です。

サブクエリで重複を除いてからカウントします。

sqlite> SELECT count(*) FROM (SELECT distinct * FROM expenses);
148

148件になりました。

詳細を見てみます

一応中身も見てみます。
ナンバリング用のカラムは作ってないのでROW_NUMBER() OVER()で出しました。
(sqlite3 ver3.31.1)

sqlite> SELECT ROW_NUMBER() OVER(), * FROM (SELECT distinct * FROM expenses);
1|08/25|カード|3,320|3000
2|08/25|入|3,142|178
3|08/25|物販|2,300|842
4|08/25|入|2,122|178
5|08/25|入|1,649|473
6|08/26|物販|1,016|633
7|08/27|物販|487|529
8|08/27|カード|3,487|3000
9|08/27|物販|2,352|1135
10|08/28|物販|1,952|400

()

140|10/08|カード|3,215|3000
141|10/08|入|3,037|178
142|10/08|入|2,564|473
143|10/08|物販|1,944|620
144|10/09|物販|1,261|683
145|10/10|物販|299|962
146|10/11|カード|3,299|3000
147|10/11|物販|1,967|1332
148|10/12|物販|967|1000

終盤のレコードもPDFのデータと合ってます。(見せられないけど)
OK。

9月分の使用額確認

8月と10月は日付が中途半端なので9月分だけ出してみます。

sqlite> SELECT description, sum(amount) FROM (SELECT distinct * FROM expenses) 
   ...> WHERE date LIKE '%09/%' GROUP BY description;
入|16406
物販|21927
カード|39000

「入」は電車賃です。交通費支給されるし私用でもあまり乗らないので気にしません。
物販がメインの使用額ですね。
今後のデータを取らないとわかりませんが、物販はほとんどコンビニ払いなので2万はちょっと使いすぎな気がします。
スーパーで買う食費とは別なのでコンビニでの買い物を改めないといけませんね。(お酒、おつまみ、煙草しか買ってないけど)

まとめ

まとめるほど長い処理は行っていませんが、さすがDBです。重複確認といった面倒くさい処理も簡単に終わりました。
あとはデータが溜まったら月ごとの集計をすればOKです。

SQL文の発行をどうするか迷います。
シンプルにshellスクリプトでも良さそうですが、インポート処理がpythonなので呼び出しもpythonで統一したほうがいいですかね。

SUICAの利用明細をDBに取り込みたい 後編

前編ではSUICAの明細のPDFの出力確認を行いました。

列ごとにデータが出力されるので各項目をリスト化してから整形します。
shellでもできそうですが、リストの扱いはプログラミング言語の方が簡単なので今回もpythonを使います。

コード

各データを整形してDB(sqlite3)に突っ込むコードです。

import re
import sqlite3
from pdfminer.high_level import extract_text

text = extract_text('20210825_20210925110904.pdf')

DATES = re.findall('\n\d\d', text)

# shaping
# Delete file output date
DATES.pop(102)
DATES.pop(51)

# Delete carry data
DATES.pop(0)
DATES.pop(-1)

mon_p1 = DATES[:50]
day_p1 = DATES[50:100]

mon_p2 = DATES[100:150]
day_p2 = DATES[150:]

category = re.findall('入|物販|カード', text)
balance = re.findall(r'\\\d,?\d{1,3}', text)
amount = re.findall('(\+\d,?\d{1,3}|\-\d,?\d{1,3})', text)

# shaping
balance.pop(0)

page1 = [x for x in zip(mon_p1, day_p1, category[:50], balance[:50], amount[:50])]
page2 = [x for x in zip(mon_p2, day_p2, category[50:], balance[50:], amount[50:])]

con = sqlite3.connect('suica_expense.db')
cur = con.cursor()
cur.execute('CREATE TABLE IF NOT EXISTS expenses (date text, description text, balance int, amount int)')

def Importfile(target):
    for i in target:
        MM = i[0].replace('\n','')
        DD = i[1].replace('\n','')
        BL = i[3].replace('\\','')
        AM = re.sub('\W','', i[4])
        cur.execute('INSERT INTO expenses VALUES (\'{}\', \'{}\', \'{}\', \'{}\')'\
                    .format(MM+'/'+DD, i[2], BL, AM))

Importfile(page1)
Importfile(page2)

con.commit()
con.close()

解説

まずは各項目のデータをリストに入れます。
月,日,種別,利用駅,残額,差額が明細として出力されますが、
月,日,種別,残額,差額(利用額)のみを格納します。

SUICAの履歴は100件分出力されますが、繰越の行が入って101件になっています。
この行は邪魔なので削除します。明細はまだ1回しかダウンロードしてないのですが、毎回出力されそうな気がします。

日付

繰越の日付を削除します。
100件分が2ページで出力されますが、50件毎に区切ると0番目と51番目が繰越の日付なのでこの位置のデータを削除します。
また数字二桁のものを集めると出力日の西暦が引っかかってしまうのでこれも削除します。(102番目と最後)

試しに5件出します。

>>> DATES[:5]
['\n08', '\n08', '\n08', '\n08', '\n08']
種別

支払いの種別です。
電車賃は入・出が1行で出力されますが、入の方だけ取れば電車賃と判別できます。
その他の支払いはすべて物販、チャージはカードで出力されます。
なので入、物販、カード(実データは半角)の文字列を取得します。

>>> category[:5]
['カード', '入', '物販', '入', '入']
残額

SUICAの残高は取っても仕方ない気がしますが、とりあえず入れておきます。
あとでグラフにでもして遊びましょう。

>>> balance[:5]
['\\3,320', '\\3,142', '\\2,300', '\\2,122', '\\1,649']
差額

実際の利用額です。
チャージしたら+、使ったら-で表記されるのでこの文字列をトリガーに取ります。

>>> amount[:5]
['+3,000', '-178', '-842', '-178', '-473']

各項目はちゃんと100件分です。(DATESは月と日が格納)

>>> len(DATES)
200
>>> len(category)
100
>>> len(balance)
100
>>> len(amount)
100
各リストのzip

各値を取得し終わったらzip化します。 各ページ50件なので、一応データも2ページ分作っておきます。

page1 = [x for x in zip(mon_p1, day_p1, category[:50], balance[:50], amount[:50])]
page2 = [x for x in zip(mon_p2, day_p2, category[50:], balance[50:], amount[50:])]

格納

sqlite3へのインポートは割愛します。 リストの値を回してSQL文に入れるだけです。

実行

これを打つだけで格納されます。

(pdf) masashi@PC-ubuntu:~/expenses$ python suica_import.py 

中身確認

sqlの出力はまだ作ってないのでsqlite3に入って確認します。

(pdf) masashi@PC-ubuntu:~/expenses$ sqlite3 suica_expense.db 
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
sqlite> .tables
expenses
sqlite> SELECT count(*) FROM expenses;
100

ちゃんと100件分格納されてます。

データはこんな感じ。

sqlite> select * from expenses;
08/25|カード|3,320|3000
08/25||3,142|178
08/25|物販|2,300|842
08/25||2,122|178
08/25||1,649|473

()

続いて各件数。

半分は電車の運賃ですね。 仕事で結構出社してました。

sqlite> SELECT description, count(*) FROM expenses GROUP BY description;
入|51
物販|35
カード|14

各項目の合計金額を出してみます。
SUICA払いはコンビニでお酒、おつまみがほとんどなので額は増えがちですね。
ここを抑えるのが節約の肝になります。

sqlite> SELECT description, sum(amount) FROM expenses GROUP BY description;
入|16762
物販|23559
カード|42000

カードはチャージ分なので除外して、純粋な使用額はこちらです。
交通費もありますが意外とかかってます。

sqlite> SELECT sum(amount) FROM expenses WHERE description IN ('', '物販');
40321

まとめ

SUICAの明細をDBに取り込むことに成功しました。
支払いのトラッキング機能としてはすでに別のツールで賄えているのであまり有効ではないかもしれませんが、
こちらもウォッチして無駄遣いをしないようにしたいと思います。

SUICAの明細は過去100件分なので都度インポートしてると重複レコードが出てくると思いますが、
DBに格納しているのでうまいこと重複を避けて計算できるはずです。 (確認は今後)

久しぶりにSQL触りましたがいろんな集計ができて楽しいですね。

おまけ

当初差額データは生データのまま検証していましたが、集計がうまくできなかったので記号を取り除きました。
+-はついたままでも計算できたのですが、カンマが入ったまま集計しているナレッジがなく、数値は数値として計算して出力の過程で編集するものなのかもしれませんね。
DBはまだまだ勉強不足です。

sqlite> SELECT * FROM expenses;
08/25|カード|3,320|+3,000
08/25|入|3,142|-178
08/25|物販|2,300|-842
08/25|入|2,122|-178
08/25|入|1,649|-473
08/26|物販|1,016|-633

sqlite> SELECT description, sum(amount) FROM expenses GROUP BY description;
入|-16762
物販|-20767.0
カード|42.0 # 明らかにおかしい

sqlite> SELECT sum(amount) FROM expenses;
-37487.0 # 集計もうまくできていない

SUICAの利用明細をDBに取り込みたい 前編

自宅の家計簿はFirefly IIIを使ってトラッキングしています。

クレジット以外の電子決済はモバイルSUICAを使用しており、SUICAのチャージもクレジット払いで利用額はわかるのですが、何にいくら使ったかがわかりません。
SUICAは電車とコンビニ払いくらいしか使っていないのですが、無駄遣いしてないかを追いたいので利用明細をDBに突っ込んで分析してみようと思います。

別にDBじゃなくてCSVとかでもいいんですが、集計が楽ですからね。

SUICAの明細

JR東日本のサイトから明細をダウンロードできます。
が、PDFでしかダウンロードできません。

まずはこれを出力させないといけませんね。

明細はこんな感じです。(一応モザイクかけます)

f:id:paloma69:20211010141543p:plain

PDFのテーブル出力

私はpythonを使えますので
「PDF テーブル python」で検索するとPDFからテーブルを出力するライブラリが主に以下の2つが出てきます。

tebulaはjavaのラッパーのようでjavaもインストールの必要ありなワードがあったのでcamelot-pyを使用することにしました。

あまり環境にあれこれ入れたくないので。

環境

サブ機のubuntuデスクトップを使います。

masashi@PC-ubuntu:~$ uname -r ; lsb_release -a
5.4.0-88-generic
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.3 LTS
Release:    20.04
Codename:   focal

camelot-pyインストール

どんな依存ライブラリが入ってくるかわからないので仮想環境作ります。

masashi@PC-ubuntu:~$ python3 -m venv pdf
masashi@PC-ubuntu:~$ source pdf/bin/activate
(pdf) masashi@PC-ubuntu:~$ python --version
Python 3.8.10
(pdf) masashi@PC-ubuntu:~$ 
(pdf) masashi@PC-ubuntu:~$ pip list
Package       Version
------------- -------
pip           20.0.2 
pkg-resources 0.0.0  
setuptools    44.0.0 
依存パッケージのインストール

公式サイトに沿って依存パッケージをインストールします。

(pdf) masashi@PC-ubuntu:~$ sudo apt install ghostscript python3-tk
camelot-pyインストール

本体をインストール。

(pdf) masashi@PC-ubuntu:~$ pip install "camelot-py[base]"
Collecting camelot-py[base]
ライブラリの確認
(pdf) masashi@PC-ubuntu:~$ pip list
Package          Version 
---------------- --------
camelot-py       0.10.1  
cffi             1.14.6  
chardet          4.0.0   
click            8.0.2   
cryptography     35.0.0  
et-xmlfile       1.1.0   
ghostscript      0.7     
numpy            1.21.2  
opencv-python    4.5.3.56
openpyxl         3.0.9   
pandas           1.3.3   
pdfminer.six     20201018
pdftopng         0.2.3   
pip              20.0.2  
pkg-resources    0.0.0   
pycparser        2.20    
PyPDF2           1.26.0  
python-dateutil  2.8.2   
pytz             2021.3  
setuptools       44.0.0  
six              1.16.0  
sortedcontainers 2.4.0   
tabulate         0.8.9   

いろいろ入ってきました。

ファイルの読み込み

こちらも公式サイトに沿ってPDFを読み込みます。
ファイル名は日付だけわかるようにリネームしてます。

>>> import camelot
>>> 
>>> tables = camelot.read_pdf('20210825_20210925110904.pdf')
>>> tables
<TableList n=0>
>>> tables.export('suica.csv', f='csv', compress=True)
>>> tables[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/masashi/pdf/lib/python3.8/site-packages/camelot/core.py", line 689, in __getitem__
    return self._tables[idx]
IndexError: list index out of range

なんとSUICAの明細はテーブルとして認識されませんでした…
困りましたねえ。

ということで他のPDF操作ライブラリ

pythonは他にもPDF操作ライブラリがあるので他の手を試してみます。

PyPDF2

開発は終わっているようなのですが使ってみます。
calelot-pyと一緒にインストールされてきました。

「Automate the Boring Stuff with Python」でも出てきます。

>>> import PyPDF2
>>> file = open('20210825_20210925110904.pdf', 'rb')
>>> pdfreader = PyPDF2.PdfFileReader(file)
>>> pdfreader.numPages
2
>>> pageobj = pdfreader.getPage(0)
>>> pageobj.extractText()
')Þ>fl>”>¢>¼ˆ‡˛«˜`#㘘>fl>”>¢>¼˜`#㘘ˆ‡˛«>fl>”>¢>¼ˆ‡˛«˜`#㘘˜`#㘘ˆ‡˛«ˆ‡˛«˜`#㘘>fl>”>¢>¼˜`#㘘ˆ‡˛«ˆ‡˛«˜`#㘘>fl>”>¢>¼>fl>”>¢>¼˜`#㘘ˆ‡˛«ˆ‡˛«˜`#㘘>fl>”>¢>¼˜`#㘘ˆ‡˛«ˆ‡˛«˜`#㘘˜`#㘘ˆ‡˛«˙v˚g8€ˆ¥'

日本語対応してませんでした…

pdfminer

とりあえず中身を読み取れればなんとかなると思い別のライブラリを試します。
pdfminerは日本語対応しているようです。

こちらもcalelot-pyと一緒にインストールされてきました。

>>> from pdfminer.high_level import extract_text
>>> text = extract_text('20210825_20210925110904.pdf')
>>> print(text)
モバイル  Suica  残額ご利用明細

JE*** **** **** 3311

残額履歴  (101件)

利用駅

種別

利用駅

月
08
08
08
08

()

読み込みは成功しましたが、行ごとではなく列順で読み込まれて出力されるようなのであまり有効ではありませんね。

pdftotext

shellにもpdfを読み込めるパッケージがあるようなのでこちらも試してみます。
コマンドを実行すると同名のtxtファイルが作成されます。

(pdf) masashi@PC-ubuntu:~/expenses$ pdftotext 20210825_20210925110904.pdf 
(pdf) masashi@PC-ubuntu:~/expenses$ ls -l 20210825_20210925110904.*
-rw-rw-r-- 1 masashi masashi 45899 109 13:11 20210825_20210925110904.pdf
-rw-rw-r-- 1 masashi masashi  4682 109 15:50 20210825_20210925110904.txt

(pdf) masashi@PC-ubuntu:~/expenses$ cat 20210825_20210925110904.txt
モバイル

Suica

残額ご利用明細
JE*** **** **** 3311

残額履歴
月
08
08
08
08
08
08
08
()

結果はpdfminerと同じですね。
これもダメか…

まとめ

SUICAの利用明細はテーブルとして出力できませんでした。
こうなったら自分でコンバートしてDBにインポートするしかありませんね。
というわけで次回に続きます。

参考サイト

Camelot: PDF Table Extraction for Humans — Camelot 0.10.1 documentation

【Python】pdfminerでPDFからテキストを抽出する | ジコログ

【 pdftotext 】コマンド――PDFファイルからテキストを抽出する:Linux基本コマンドTips(286) - @IT

メイン機Windowsを新調しました

主に映画やゲーム用途のメイン機と呼んでいるWindows10が壊れてしまったので新調しました。
電源の故障で、半年くらい前から急にフリーズしたりOS起動中に停止したりという事象が発生して先週ついに電源ランプがつかなくなってしまいました。

サブのPCやスマホがあるのでInternet周りのサービスには困りませんが、
ゲームができないと困ります(趣味なので)

というわけで今週の末に新しいPCが届いたのでリプレースをしました。
新調と言いながらヤフオクでお安くあげてますw

旧マシンはロープロファイルだったのですが、その分電源も小さく多分ゲームで電源を酷使してしまったと思われるので今回は普通サイズのものにしました。
電源は調べてませんが、以前より容量は大きいと思います。

外観

筐体にメーカーのロゴがないので自作のものを購入した感じですかね。
出品欄にこの辺の説明がなくて特に気にせず買いました。
結構かっこいいです。
グレア加工で反射してますw

f:id:paloma69:20211003172917j:plain

マザボは後ほど。

パーツ移植

故障機のパーツを載せ替えます。

  • メモリ
    • 購入時4GB + 追加8GB -> 12GBへ
  • HDD
    • データ救出用にマウント
  • グラボ

このスペックでGTAオンラインが快適に動きますので、買い替える必要もなく旧マシンとそんなに変わりません。
CPUはちょっと世代が上がりました。

いつもロープロファイルを買ってたので久しぶりに普通サイズのPCを触ります。
ディスクを複数マウントできるのは便利ですね。

グラボのブラケットも付け替えました。

before

f:id:paloma69:20211003171814j:plain

after

f:id:paloma69:20211003171840p:plain

わざわざ乗せるものでもないですが。

起動

起動後、各パーツが見えました。

f:id:paloma69:20211003172018p:plain

OSのバージョン確認、Microsoftアカウント作業系は割愛します。

PC環境復旧!

壁紙も付け替えて旧マシンと同じ環境に戻りました。
壁紙は「saints row4 wall paper」で検索すると出てくるバーチャルスティールポートという街です。
ゲーム中こんな街並み無かった気がするのでどこかの実写から色見だけ合わせた感じでしょうか。
この壁紙かっこよくて気に入ってます。
そういえばSaints rowも新作発表されましたね!

f:id:paloma69:20211003172055p:plain

ちなみにアプリをちまちまダウンロードするのは面倒なのでNiniteというサイトで一括インストールしてます。
ダウンロードしたいパッケージを選ぶと、まとまったインストーラーがダウンロードできます。

ninite.com

とある日知ってから活用してます。

マシンのスペック

PowerShellで取得した情報です。

マザーボード

ヤフオクではB85H3-M4:V2.0で紹介されてました。
MouseComputer製のようですね。

PS C:\Users\masashi> get-wmiobject win32_computersystemproduct


IdentifyingNumber : 00000000
Name              : B85H3-M4
Vendor            : MouseComputer/S81
Version           : 1.0/2.0
Caption           : コンピューター システム製品
CPU

旧マシンと同じCore i5ですが第4世代に上がりましたw

PS C:\Users\masashi> get-wmiobject win32_processor


Caption           : Intel64 Family 6 Model 60 Stepping 3
DeviceID          : CPU0
Manufacturer      : GenuineIntel
MaxClockSpeed     : 3101
Name              : Intel(R) Core(TM) i5-4440 CPU @ 3.10GHz
SocketDesignation : SOCKET 0
  • 旧マシン
PS C:\Users\tsuru> get-wmiobject win32_processor


Caption           : Intel64 Family 6 Model 42 Stepping 7
DeviceID          : CPU0
Manufacturer      : GenuineIntel
MaxClockSpeed     : 3101
Name              : Intel(R) Core(TM) i5-2400 CPU @ 3.10GHz
SocketDesignation : SOCKET 0
メモリ

12Gです。
このマザボは最大16Gまでなので上げずにこのままかもしれません。

PS C:\Users\masashi> get-wmiobject win32_computersystem


Domain              : WORKGROUP
Manufacturer        : MouseComputer/S81
Model               : B85H3-M4
Name                : DESKTOP-HBP3520
PrimaryOwnerName    : user
TotalPhysicalMemory : 12804370432
  • 旧マシン
PS C:\Users\tsuru> get-wmiobject win32_computersystem


Domain              : WORKGROUP
Manufacturer        : ONKYO CORPORATION
Model               : ONKYOPC
Name                : DESKTOP-986MNSO
PrimaryOwnerName    :
TotalPhysicalMemory : 12866777088

まとめ

急にフリーズする心配もなくなり快適なPCライフが戻ってきました。
この子にもゲームに映画に活躍してもらいます。
以前のデータも別ドライブで扱えるのでいい感じに移植できました。

先月はスマホもバッテリー故障で買い替えた矢先にPCの故障だったので、やな感じの月でした。
今月は平和に過ごしたいですね。

powershellのループいろいろ

仕事でteratermマクロを定期ループさせて機器のステータスを取る用事があったのですが、最近よく使うpowershellから起動してみることにしました。

powershellはループ方法が多いので書いておきます。

環境

最近また急にハングするようになった調子悪いメイン機Windowsです。

  • OS
PS C:\Users\tsuru> Get-WmiObject Win32_OperatingSystem


SystemDirectory : C:\WINDOWS\system32
Organization    :
BuildNumber     : 19042
RegisteredUser  :
SerialNumber    : 00326-10000-00000-AA530
Version         : 10.0.19042
PS C:\Users\tsuru> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      5.1.19041.1237
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.19041.1237
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1

今回はループ処理の確認なのでマクロ起動文は割愛してdateコマンドを10回繰り返す、というスクリプトを書きます。
簡単なのでワンライナーで書いちゃいます。

while

shellでもよく使います。条件式を満たすまでですね。

PS C:\Users\tsuru> $a=1 ; while ( $a -le 10 ) { date ; sleep 1 ; $a++ }

2021923日 11:51:19
2021923日 11:51:20
2021923日 11:51:21
2021923日 11:51:23
2021923日 11:51:24
2021923日 11:51:25
2021923日 11:51:26
2021923日 11:51:27
2021923日 11:51:28

for

こちらもよく使います。カウンタの加算など、条件式の書き方がシンプルですね。

PS C:\Users\tsuru> for ( $a=1; $a -le 10; $a++ ) { date ; sleep 1 }

2021923日 11:54:49
2021923日 11:54:50
2021923日 11:54:51
2021923日 11:54:52
2021923日 11:54:53
2021923日 11:54:54
2021923日 11:54:55
2021923日 11:54:56
2021923日 11:54:57
2021923日 11:54:58

foreach

リスト系の展開によく使ってます。
反復項目をあらかじめメモリに格納するそうです。知らなかった。

PS C:\Users\tsuru> foreach ( $a in 1..10 ) { date ; sleep 1 }

2021923日 11:55:25
2021923日 11:55:26
2021923日 11:55:27
2021923日 11:55:28
2021923日 11:55:29
2021923日 11:55:30
2021923日 11:55:31
2021923日 11:55:32
2021923日 11:55:33
2021923日 11:55:34

do

最初に処理を書いて後で条件式を書くという珍しめ?な形です。
最後の条件式にwhile(trueの間), until(falseの間)が指定出来るようです。

  • while
PS C:\Users\tsuru> $a=1 ; do { date ; sleep 1 ; $a++ } while ( $a -le 10 )

2021923日 11:56:03
2021923日 11:56:04
2021923日 11:56:05
2021923日 11:56:06
2021923日 11:56:07
2021923日 11:56:08
2021923日 11:56:09
2021923日 11:56:10
2021923日 11:56:11
2021923日 11:56:12
  • until

falseの間10回繰り返したいので11以上になったら停止です。 あまりfalseの間、という処理を書いたことが無いですね。

PS C:\Users\tsuru> $a=1 ; do { date ; sleep 1 ; $a++ } until ( $a -gt 10 )

2021923日 11:57:01
2021923日 11:57:02
2021923日 11:57:03
2021923日 11:57:04
2021923日 11:57:05
2021923日 11:57:06
2021923日 11:57:07
2021923日 11:57:08
2021923日 11:57:09
2021923日 11:57:10

本題 無限ループを回す

powershellでもシンプルにwhile trueでできました。
$trueと書かないと真偽値として扱われないのですこーしはまりました。

PS C:\Users\tsuru> while ($true) { date ; sleep 1 }

2021923日 11:57:53
2021923日 11:57:54
2021923日 11:57:55
2021923日 11:57:56
2021923日 11:57:57
2021923日 11:57:58
2021923日 11:57:59
2021923日 11:58:00
2021923日 11:58:01
2021923日 11:58:02
2021923日 11:58:03
2021923日 11:58:04
2021923日 11:58:05
2021923日 11:58:06
2021923日 11:58:07
2021923日 11:58:08
2021923日 11:58:09
2021923日 11:58:10
PS C:\Users\tsuru>

Ctrl-cで停止です。

まとめ

簡単ですがpowershellのループまとめです。

コマンドプロンプトのループよりほんの少し分かりやすいのでこちらの方が好みです。
ループを覚えておくと反復処理の小技が出来ますのでpowershellでも押さえておきたいですね。

参考サイト

フロー制御 - PowerShell | Microsoft Docs

strongswanとAndroidのネイティブVPNでIPsecをする

前回の記事でsshポートフォワードで無理やりVPNを作りましたが、本当はstrongswanでのIPsecを考えていたので今回ちょっと紹介します。

UbuntuへのVPN

Androidからpfsense向けは経験ありなので出来るのですが、今回の対象はubuntuのLXD環境です。

pfsenseはisoしか提供されてないのでvirtualbox等で無理やり作るよりはおとなしくLXDホストにVPNサーバを立てることにしました。

候補としてはSoftEther、WireGuard、strongswanがありましたが、strongswanにしました。
設定的に専用クライアントを入れなくてもネイティブのツールで出来そうだったからです。

ネイティブのツールだとアルゴリズムがちょっと弱いですが、インストール無しで出来るのが楽でいいですね。

strongswanの設定

インストールは割愛して設定内容。

Version
  • VPNサーバとなるLXDホスト機
masashi@PC-ubuntu:~$ uname -a
Linux PC-ubuntu 5.4.0-81-generic #91-Ubuntu SMP Thu Jul 15 19:09:17 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

masashi@PC-ubuntu:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
Codename:       focal
  • strongswan

aptで入れたらswanctlが無かったのでipsecコマンドを使います。

masashi@PC-ubuntu:~$ ipsec --version
Linux strongSwan U5.8.2/K5.4.0-81-generic
University of Applied Sciences Rapperswil, Switzerland
See 'ipsec --copyright' for copyright information.
ipsec.conf

アルゴリズムはpfsenseのドキュメントを参考に同パラメータにしています。 認証方法はPre Shared Key + Xauthです。

# ipsec.conf - strongSwan IPsec configuration file

# basic configuration

config setup
        # strictcrlpolicy=yes
        # uniqueids = no

# Add connections here.
conn %default
        ikelifetime=60m
        keylife=20m
        rekeymargin=3m
        keyingtries=1
        keyexchange=ikev1

conn android
        ikelifetime=24h
        keylife=8h
        aggressive=yes
        ike=aes128-sha1-modp1024
        esp=aes128-sha1
        left=192.168.0.10
        leftsubnet=192.168.0.0/24
        leftauth=psk
        right=%any
        rightauth=psk
        rightauth2=xauth
        rightsourceip=10.255.0.0/24
        auto=add
  • left: サーバ側
  • right: クライアント側

です。

サイト間ではなくリモートアクセスなのでアグレッシブモード。

アグレッシブモードにするにはドキュメントにcharon.confも編集しろとあったのでいじってます。

  • charon.conf
masashi@PC-ubuntu:~$ grep dont_care /etc/strongswan.d/charon.conf
    i_dont_care_about_security_and_use_aggressive_mode_psk = yes

送信元Anyからikeを受け付けて認証後にトンネル用の10.255.0.Xを払いだすという設定です。
rightsourceipを設定してあげないと認証でこけました。

ipsec.secret

PSKとXauthのパスワードです。

# This file holds shared secrets or RSA private keys for authentication.

# RSA private key for this host, authenticating it to any other host
# which knows the public part.

192.168.0.10 %any : PSK <pre shared key>

masashi : XAUTH <password>

接続してみる

同じLANですがAndroidから無事繋がりました。

f:id:paloma69:20210828143858p:plain

接続後ステータス

Androidのはターミナル等入れてないので確認できませんが、サーバ側はこんな感じ。
IPsecは張れましたが、LXD内のサーバにアクセスしていないので実際のデータ転送は未検証です。 ルーティングインストールとか上手くできているんでしょうか?

masashi@PC-ubuntu:~$ sudo ipsec statusall
Status of IKE charon daemon (weakSwan 5.8.2, Linux 5.4.0-81-generic, x86_64):
  uptime: 2 days, since Aug 25 23:43:02 2021
  malloc: sbrk 2703360, mmap 0, used 804256, free 1899104
  worker threads: 11 of 16 idle, 5/0/0/0 working, job queue: 0/0/0/0, scheduled: 14
  loaded plugins: charon aesni aes rc2 sha2 sha1 md5 mgf1 random nonce x509 revocation constraints pubkey pkcs1 pkcs7 pkcs8 pkcs12 pgp dnskey sshkey pem openssl fips-prf gmp agent xcbc hmac gcm drbg attr kernel-netlink resolve socket-default connmark stroke updown eap-mschapv2 xauth-generic counters
Virtual IP pools (size/online/offline):
  10.255.0.0/24: 254/1/0
Listening IP addresses:
  192.168.0.10
  10.28.57.1
  fd42:24ab:b835:ae42::1
Connections:
     android:  192.168.0.10...%any  IKEv1 Aggressive
     android:   local:  [192.168.0.10] uses pre-shared key authentication
     android:   remote: uses pre-shared key authentication
     android:   remote: uses XAuth authentication: any
     android:   child:  192.168.0.0/24 === dynamic TUNNEL
Security Associations (1 up, 0 connecting):
     android[7]: ESTABLISHED 57 seconds ago, 192.168.0.10[192.168.0.10]...192.168.0.15[moon.strongswan.org]
     android[7]: Remote XAuth identity: masashi
     android[7]: IKEv1 SPIs: 005e02f5920b8651_i 95777d60fe83c0aa_r*, pre-shared key reauthentication in 23 hours
     android[7]: IKE proposal: AES_CBC_128/HMAC_SHA1_96/PRF_HMAC_SHA1/MODP_1024
     android{8}:  INSTALLED, TUNNEL, reqid 7, ESP SPIs: ca903b04_i 0c287df6_o
     android{8}:  AES_CBC_128/HMAC_SHA1_96, 4024 bytes_i, 0 bytes_o, rekeying in 7 hours
     android{8}:   192.168.0.0/24 === 10.255.0.1/32

Androidの設定

ネイティブのVPNクライアントです。

f:id:paloma69:20210828145040p:plain

割愛しますが接続方法は以前のこちらから。

AndroidからpfSenseにIPsec接続して内部NWにアクセスする - paloma blog

キャプチャ

tcpdumpはこんな感じ。ESPが確認できませんね。
ログ上クイックモードになってるから?
たまに触ると忘れちゃってますね。

masashi@PC-ubuntu:~$ sudo tcpdump -nni enp2s0 port 500 or port 4500
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp2s0, link-type EN10MB (Ethernet), capture size 262144 bytes

14:27:09.709073 IP 192.168.0.15.500 > 192.168.0.10.500: isakmp: phase 1 I agg
14:27:09.712766 IP 192.168.0.10.500 > 192.168.0.15.500: isakmp: phase 1 R agg
14:27:09.721536 IP 192.168.0.15.500 > 192.168.0.10.500: isakmp: phase 1 I agg
14:27:09.721629 IP 192.168.0.15.500 > 192.168.0.10.500: isakmp: phase 2/others I inf[E]
14:27:09.722114 IP 192.168.0.10.500 > 192.168.0.15.500: isakmp: phase 2/others R #6[E]
14:27:09.724073 IP 192.168.0.15.500 > 192.168.0.10.500: isakmp: phase 2/others I #6[E]
14:27:09.724586 IP 192.168.0.10.500 > 192.168.0.15.500: isakmp: phase 2/others R #6[E]
14:27:09.726540 IP 192.168.0.15.500 > 192.168.0.10.500: isakmp: phase 2/others I #6[E]
14:27:09.726631 IP 192.168.0.15.500 > 192.168.0.10.500: isakmp: phase 2/others I #6[E]
14:27:09.727315 IP 192.168.0.10.500 > 192.168.0.15.500: isakmp: phase 2/others R #6[E]
14:27:10.347408 IP 192.168.0.15.500 > 192.168.0.10.500: isakmp: phase 2/others I oakley-quick[E]
14:27:10.348159 IP 192.168.0.10.500 > 192.168.0.15.500: isakmp: phase 2/others R oakley-quick[E]
14:27:10.401565 IP 192.168.0.15.500 > 192.168.0.10.500: isakmp: phase 2/others I oakley-quick[E]

まとめ

strongswanとAndroidIPSecの確認でした。
LAN内での接続なのでパラメータのチューニングは余地ありですが、
AndroidのネイティブアプリでとりあえずのVPN接続ができました。
また、今回ログ周りの編集はしていないのでデーモンのログは/var/log/syslogに吐かれます。

これでInternet経由で接続出来たら完璧なのですが、取り急ぎはSSHポートフォワードで凌ぎつつ
レンタルルータをいじってゆくゆくはIPSecに移行できたらと思います。