19日に更新してた

アフィリエイトはないよ

【python & golang】Yahoo!ファイナンスVIP倶楽部のCSV、下り最速

どこかの豆腐屋みたいなタイトルにしておりますが、実態はへっぽこでして。

東証にあるエクセルファイルをダウンロードして、リストを作ろうと python ノリで golang をいじってみたのですが、130A.Tとかアルファベット表記が入っている番号を golang で取り込むのが僕には無理だった*1ので python pandas を使って print したのを取り込む形にしました。golang のアプリと同じディレクトリにおいておきます。

pro market のものはヤフーファイナンスにて出てこなかったので、取り除いてあります。

>>tosho.py
import pandas as pd
a=pd.read_excel("https://www.jpx.co.jp/markets/statistics-equities/misc/tvdivq0000001vg2-att/data_j.xls")
print(",".join([str(s) for s in a["コード"][a["市場・商品区分"]!="PRO Market"]]))

ここからが難儀の始まり

  • tab を複数開くと処理時間がかかるだろうと思い、1枚だけにして更新。
  • SSD 上にユーザープロファイルもダウンロードファイルも置いて実行するとまとまった数が落とせるようになる。
  • エラー率が高かったので、クリックしてダウンロードが終了したら次に画面読み込みに行くようにする。
  • ヘッドレスにしたらうまくいくかと思うも、動かないので表示を出す。
  • 650-700 ファイルくらいダウンロードすると止まる。平均ダウンロード時間が sleep 抜きで2秒を超えると進まなくなる。chrome の設定では変化なし。
  • 1ファイルごとに sleep を 0.5 秒いれると 1000程度落とせるようになる。平均ダウンロード時間が sleep 抜きで2秒を超えると進まなくなるのは変わりない。
  • どうにもならないのでダウンロード検出、sleep は消し、1ファイル1tab にて落とすようにしたらダウンロードできるようになった。1ファイルあたりの平均ダウンロード時間も早くなる。tab の close() を明示的にやらないといけないがある程度の時間を取っておかないと行けないので、goroutine にて閉めるようにする。
  • 電源設定を変えてないのでスリープすると落ちるのでマウスジグラー的なものを入れてみる。自分の環境だと座標指定してもよくわからない動きをするのでランダムで動かす。
  • 動くには動くが3000ファイルくらい落とすと、画面表示は出るがクリックまでの速度が遅くなる。600ファイル位から10ファイルに1つから2つくらい3-7秒かかるようになる。
  • 並列ダウンロードできるように組んだらいいのかとやってみるがループだけ回って全くダウンロードしない。
  • ガベージコレクション動かしたらいいのか?と runtime.GC() やってみるも変わらず。page.ClearCompilationCache()も同じく。
  • go get -u github.com/chromedp/chromedp して go: upgraded github.com/chromedp/chromedp v0.9.3 => v0.9.5 変わらず。
  • なぜかポートフォリオが作られていたので削除。変化なし。
  • chrome の設定のメモリセーバーをオンにする。変わらず。
  • 単なるループだとある程度ダウンロードできるようになる。
  • chrome を バージョン: 123.0.6312.123(Official Build) (64 ビット)にしたらある程度うまくいくようになったが、広告の表示が出るときに時間がかかっているので adblock をいれた。うまくいきそうだったので放置して寝たら、あと1ファイルというところでアプリが落ちて終わっていた。\r\n の改行が入っていた模様。
  • 画面描画に時間がかかるのでは?と考え描画が終わったあたりで次を読み込むといいのかもと、sleep を入れて goroutine を使ってみる。950ms 位を入れないとうまくダウンロードできないが、まれにダウンロード数が足りないのでそのあたりを補足。
  • 時間帯によってダウンロードスピードが変化する。夜ダウンロードすると速い。
package main

import (
	"context"
	"fmt"
	"log"
	"math/rand"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"slices"
	"strings"
	"sync"
	"time"

	"github.com/chromedp/cdproto/browser"
	"github.com/chromedp/cdproto/cdp"
	"github.com/chromedp/chromedp"
	"github.com/go-vgo/robotgo"
)

// ランダムで動かしている。
func mouseziggler() {
	robotgo.MouseSleep = 100
	x, y := rand.Intn(1920), rand.Intn(1080)
	robotgo.Move(x, y)
}

func main() {
	s := time.Now()

	p, _ := os.UserHomeDir()

	// download directory
	wd := filepath.Join(p, "Downloads", "csv")

	// userdatadir
	UserDataDir := filepath.Join(p, "AppData", "Local", "Google", "Chrome")

	// tosho.pyからの読み込み
	stdout, err := exec.Command("python", "tosho.py").CombinedOutput()
	if err != nil {
		log.Fatalln(err)
	}
	tmpIds := strings.TrimRight(string(stdout), "\r\n")
	ids := strings.Split(tmpIds, " ")

	const yf string = "https://finance.yahoo.co.jp"

	// 新しいコンテキストを作成
	allcCtx, cancel := chromedp.NewExecAllocator(context.Background(), []chromedp.ExecAllocatorOption{
		// headlessいれると動かない
		// chromedp.Flag("headless", true),
		chromedp.NoFirstRun,
		chromedp.NoDefaultBrowserCheck,
		chromedp.UserDataDir(UserDataDir),
	}...)
	defer cancel()

	ctx, cancel := chromedp.NewContext(allcCtx)
	defer cancel()

	// ブラウザを起動
	err = chromedp.Run(ctx,
		chromedp.Navigate(yf),
		// ダウンロードディレクトリを設定
		// browser.SetDownloadBehaviorBehaviorAllowAndName にしない
		browser.
			SetDownloadBehavior(browser.SetDownloadBehaviorBehaviorAllow).
			WithDownloadPath(wd).
			WithEventsEnabled(true),
	)
	if err != nil {
		log.Fatalln(err)
	}

	// (NEXT FUNDS)ロシア株式指数上場投信【1324.T】
	l := []string{"1324"}

	var wg sync.WaitGroup

	const concurrency = 2
	limit := make(chan struct{}, concurrency)

	for i, id := range ids {
		// test用
		// if i >= 100 {
		// 	break
		// }

		if slices.Contains(l, id) {
			continue
		}

		time.Sleep(950 * time.Millisecond)

		rap := time.Since(s).Seconds()
		wg.Add(1)

		f := func(i int, id string, ctx context.Context, rap float64) {

			if i%100 == 0 {
				// マウスジグラー的なもの
				mouseziggler()
			}

			id = strings.ReplaceAll(id, " ", "")
			id += ".T"
			url, _ := url.JoinPath(yf, "quote", id, "history")

			idf := id + ".csv"

			tabctx, cancel := chromedp.NewContext(ctx)

		L:
			limit <- struct{}{}
		L2:
			var nodes []*cdp.Node

			// タブでダウンロードを行う
			err = chromedp.Run(tabctx,
				chromedp.Navigate(url),
				chromedp.Nodes("#csv_dl", &nodes, chromedp.NodeVisible, chromedp.AtLeast(0)),
			)
			if err != nil {
				log.Fatal(err)
			}

			if len(nodes) > 0 {
				chromedp.Run(tabctx,
					chromedp.Click("#csv_dl > a", chromedp.NodeVisible),
				)
				<-limit
			} else {
				goto L2
			}

			time.Sleep(5 * time.Second)

			if _, err := os.Stat(filepath.Join(wd, idf)); err != nil {
				goto L
			} else {
				wg.Done()
				cancel()
				avr := time.Since(s).Seconds() / float64(i+1)
				dif := time.Since(s).Seconds() - rap
				rem := avr * float64(len(ids)-(i+1))
				fmt.Printf("%v, %v, %.2f, %.4f, %.2f, %.2f \n", i, id, time.Since(s).Seconds(), avr, dif, rem)
			}

		go f(i, id, ctx, rap)

	}

	wg.Wait()

	fmt.Println("Done.")

	dir, _ := os.ReadDir(wd)


	fmt.Printf("time\t\t: %.1f sec\ntime per ID\t: %.2f sec \n", time.Since(s).Seconds(), time.Since(s).Seconds()/float64(len(dir)))

	fmt.Printf("download files\t: %d files\nlost files\t: %d files\n", len(dir), len(ids)-len(dir)-len(l))
}

で結果が

time            : 4068.3 sec
time per ID     : 0.96 sec
download files  : 4251 files
lost files      : 0 files

こんな感じ。

*1:xls のライブラリ2つ試してみたのですができませんでした