Skip to content

Commit

Permalink
Merge pull request #3 from azuki774/cf-import
Browse files Browse the repository at this point in the history
add: money-forward-cf
  • Loading branch information
azuki774 committed Jul 31, 2024
2 parents c9478fd + 528cc80 commit dfb2575
Show file tree
Hide file tree
Showing 15 changed files with 482 additions and 7 deletions.
40 changes: 40 additions & 0 deletions .github/workflows/image-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,43 @@ jobs:
file: ./build/sbi/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}

build_and_push_mf:
runs-on: ubuntu-latest
env:
IMAGE_NAME: myscrapers-mf
steps:
- name: checkout
uses: actions/checkout@v4

- name: Set meta
id: meta
uses: docker/metadata-action@v4
with:
# list of Docker images to use as base name for tags
images: |
ghcr.io/azuki774/myscrapers-mf
# generate Docker tags based on the following events/attributes
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=semver,pattern=latest
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GH_ACCESS_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./build/moneyforward/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ deployment/browser/
deployment/*.jpg

!.gitkeep

compose.yml
*-token.env
tmp
6 changes: 4 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
SHELL=/bin/bash
VERSION=latest
container_name=myscrapers
container_name_sbi=myscrapers-sbi
container_name_mf=myscrapers-mf

.PHONY: build bin-linux-amd64 start stop debug setup lint test

Expand All @@ -12,7 +13,8 @@ bin-linux-amd64:
-o build/bin/ ./...

build:
docker build -t $(container_name):$(VERSION) -f build/sbi/Dockerfile .
docker build -t $(container_name_sbi):$(VERSION) -f build/sbi/Dockerfile .
docker build -t $(container_name_mf):$(VERSION) -f build/moneyforward/Dockerfile .

start:
docker compose -f deployment/compose.yml up -d
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,17 @@
- outputDir オプションがあった場合は、${outputDir}/YYYYMMDD_x.csv
- s3ストレージにアップロードへの機能がある。
- 環境変数 `BUCKET_NAME` があった場合、取得したデータを `s3://${BUCKET_NAME}/${REMOTE_DIR}/YYYYMMDD/` に保存。

## Quick start (binary)

```
docker run --rm -p 7327:7327 ghcr.io/go-rod/rod:v0.116.2
```

```
user=<your id> \
pass=<your pass> \
outputDir="." \
wsAddr="localhost:7327" `
build/bin/myscrapers download moneyforward
```
24 changes: 24 additions & 0 deletions build/moneyforward/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM golang:1.21.0 as builder

COPY . /app/
WORKDIR /app

# Go build
RUN go mod download
RUN make bin-linux-amd64

FROM debian:bookworm-slim as runner
# Required Packages
RUN apt-get update && \
apt-get install -y curl unzip && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

# AWS Setup
RUN curl -o /var/tmp/awscli.zip https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip && \
unzip -d /usr/local/bin/ /var/tmp/awscli.zip

RUN mkdir -p /usr/local/bin && mkdir -p /data/
COPY --from=builder /app/build/bin/myscrapers /usr/local/bin/myscrapers
COPY --chmod=755 build/moneyforward/main.sh /usr/local/bin/main.sh
ENTRYPOINT ["/usr/local/bin/main.sh"]
61 changes: 61 additions & 0 deletions build/moneyforward/main.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/bin/bash
set -e
YYYYMM=`date '+%Y%m'`
YYYYMMDD=`date '+%Y%m%d'`

# BUCKET_URL # from env (ex: "https://s3.ap-northeast-1.wasabisys.com")
# BUCKET_NAME # from env (ex: hoge-system-stg-bucket)
# BUCKET_DIR # from env (ex: fetcher/moneyforward)
# AWS_REGION # from env (ex: ap-northeast-1)
# AWS_ACCESS_KEY_ID # from env
# AWS_SECRET_ACCESS_KEY # from env
# user="xxxxxxxxx" # moneyforward id , from env
# pass="yyyyyyyyy" # moneyforward pass, from env
# wsAddr # from env (ex: localhost:7327)

SCRAPERS_BIN="/usr/local/bin/myscrapers"
AWS_BIN="/usr/local/bin/aws/dist/aws"
outputDir="/data/${YYYYMM}/${YYYYMMDD}"

REMOTE_DIR="${BUCKET_DIR}/${YYYYMM}/${YYYYMMDD}"

function download () {
echo "job start"
mkdir -p ${outputDir}
echo "output to dir: ${outputDir}"
outputDir=${outputDir} \
user=${user} \
pass=${pass} \
${SCRAPERS_BIN} download moneyforward --lastmonth
echo "job complete"
}

function create_s3_credentials () {
echo "s3 credentials create start"
mkdir -p ~/.aws/

echo "[default]" >> ~/.aws/config
echo "region = ${AWS_REGION}" >> ~/.aws/config

echo "[default]" >> ~/.aws/credentials
echo "aws_access_key_id = ${AWS_ACCESS_KEY_ID}" >> ~/.aws/credentials
echo "aws_secret_access_key = ${AWS_SECRET_ACCESS_KEY}" >> ~/.aws/credentials

chmod 400 ~/.aws/config
chmod 400 ~/.aws/credentials
ls -la ~/.aws/
echo "s3 credentials create complete"
}

function s3_upload () {
echo "s3 upload start"
${AWS_BIN} s3 cp ${outputDir}/ "s3://${BUCKET_NAME}/${REMOTE_DIR}" --recursive --endpoint-url="${BUCKET_URL}"
echo "s3 upload complete"
}

download

if [ -n $BUCKET_NAME ]; then
create_s3_credentials
s3_upload
fi
1 change: 1 addition & 0 deletions build/sbi/main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ YYYYMMDD=`date '+%Y%m%d'`
# AWS_REGION # from env (ex: ap-northeast-1)
# AWS_ACCESS_KEY_ID # from env
# AWS_SECRET_ACCESS_KEY # from env
# wsAddr # from env (ex: localhost:7327)

SCRAPERS_BIN="/usr/local/bin/myscrapers"
AWS_BIN="/usr/local/bin/aws/dist/aws"
Expand Down
16 changes: 13 additions & 3 deletions cmd/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"log/slog"
"myscrapers/internal/scenario"
"os"

"github.com/spf13/cobra"
)
Expand All @@ -15,6 +14,7 @@ var downloadArgsOption downloadArgsOpt
type downloadArgsOpt struct {
SiteName string
OutputDir string
LastMonth bool
}

// downloadCmd represents the download command
Expand Down Expand Up @@ -46,6 +46,17 @@ func startDownload(opts downloadArgsOpt) (err error) {
slog.Error("failed to scrape", "err", err.Error())
return err
}
slog.Info("download sbi complete")
case "moneyforward":
mf, err := scenario.NewScenarioMoneyForward(downloadArgsOption.LastMonth)
if err != nil {
return err
}
if err = mf.Start(ctx); err != nil {
slog.Error("failed to scrape", "err", err.Error())
return err
}
slog.Info("download moneyforward complete")
case "test-github":
sc := scenario.NewTestGitHub()
return sc.Start(ctx)
Expand All @@ -56,7 +67,6 @@ func startDownload(opts downloadArgsOpt) (err error) {
}

func init() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: true}))
slog.SetDefault(logger)
rootCmd.AddCommand(downloadCmd)
downloadCmd.Flags().BoolVarP(&downloadArgsOption.LastMonth, "lastmonth", "l", true, "fetch last month") // 先月分も読み込むかどうか(月末取りこぼしを防ぐため、基本は true)
}
3 changes: 3 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"log/slog"
"os"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -40,4 +41,6 @@ func init() {
// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: true}))
slog.SetDefault(logger)
}
13 changes: 13 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## moneyforward-cf

### output CSV
例:
```
計算対象,日付,内容,金額(円),保有金融機関,大項目,中項目,メモ,振替,削除
,07/16(火),ローソン,-291,三井住友カード,食費,食料品,,,
,07/16(火),GITHUB,-158,JCBカード,通信費,情報サービス,,,
,07/10(水),マクドナルド,-600,三井住友カード,食費,外食,,,
```

### 出力先
- コンテナ内デフォルト: `/data/YYYYMM/YYYYMMDD/cf.csv`, `--lastmonth` 付与時は、`/data/YYYYMM/YYYYMMDD/cf_lastmonth.csv` も出力。
7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ module myscrapers
go 1.21.0

require (
github.com/go-rod/rod v0.116.2 // indirect
github.com/go-rod/rod v0.116.2
github.com/spf13/cobra v1.8.1
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/ysmood/fetchup v0.2.3 // indirect
github.com/ysmood/goob v0.4.0 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=
github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns=
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg=
github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q=
github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg=
github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY=
github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM=
github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
Expand Down
114 changes: 114 additions & 0 deletions internal/importer/moneyforward.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package moneyforward

import (
"context"
"fmt"
"log/slog"
"myscrapers/internal/csv"
"strings"

"github.com/go-rod/rod"
)

const cfFieldSize = 10

func validateCF(header []string, bodies [][]string) error {
if len(header) != cfFieldSize {
return fmt.Errorf("invalid field size: header")
}
for i, b := range bodies {
if len(b) != cfFieldSize {
return fmt.Errorf("invalid field size: bodies #%d", i+1)
}
}
return nil
}

// cfPage は /cf のページを rod で取得したもの
func getHeader(ctx context.Context, cfPage *rod.Page) (header []string, err error) {
cfDetailTable, err := cfPage.Element(`[id=cf-detail-table]`)
if err != nil {
slog.Error("failed to get cf-detail-table")
return []string{}, err
}

ths, err := cfDetailTable.Elements("th")
if err != nil {
slog.Error("failed to get cfDetailTable")
return []string{}, err
}

for _, th := range ths {
// セレクターの選択肢のテキストを消す
txt := strings.Split(th.MustText(), " ")[0]
// 改行を消す
txt = strings.ReplaceAll(txt, "\n", "")
// 無駄な空白を消す
txt = strings.ReplaceAll(txt, " ", "")
header = append(header, txt)
}

return header, nil
}

// cfPage は /cf のページを rod で取得したもの
func getBody(ctx context.Context, cfPage *rod.Page) (bodies [][]string, err error) {
cfDetailTable, err := cfPage.Element(`[id=cf-detail-table]`)
if err != nil {
slog.Error("failed to get cf-detail-table")
return [][]string{}, err
}

recordRows, err := cfDetailTable.Elements(`[class="transaction_list js-cf-edit-container target-active"`) // 1行ごとのrecordsのフィールドを特定する
if err != nil {
slog.Error("failed to get recordRows")
return [][]string{}, err
}

for _, recordRow := range recordRows {
var row []string
spans := recordRow.MustElements("td") // 1行ごとのレコードから各セルを抽出
for _, span := range spans {
// セレクターはないので何も消さない
// 改行を消す
txt := strings.ReplaceAll(span.MustText(), "\n", "")
// 無駄な空白を消さない
row = append(row, txt)
}
bodies = append(bodies, row)
}

return bodies, nil
}

func ImportStart(ctx context.Context, filePath string, page *rod.Page) (err error) {
var header []string
var bodies [][]string

header, err = getHeader(ctx, page)
if err != nil {
slog.Error("failed to get header")
return err
}
slog.Info("get CSV header")

bodies, err = getBody(ctx, page)
if err != nil {
slog.Error("failed to get bodies")
return err
}
slog.Info("get CSV body")

// validation
if err := validateCF(header, bodies); err != nil {
return err
}

// csv書き込み
if err := csv.WriteFile(filePath, header, bodies); err != nil {
slog.Error("failed to output csv")
return err
}
slog.Info("output csv complete", "outputPath", filePath)
return nil
}
Loading

0 comments on commit dfb2575

Please sign in to comment.