株価スクレイピング

VBAで株価スクレイピングしようかと思ったんですが、検索すると圧倒的にPythonがヒットするので、どれだけ高速なのか調査も兼ねてPythonの開発環境を整備して、ある程度動作できるところまでやってみました。

ただ、もう面倒くさい。インタープリターベースの言語がなんでこんなに人気があるのか理解できないです。記述もなんというか美しくないんですよね。新たに言語を覚えるならコンパイラベースのC#とかVB#がいいと思うんですが、どうなんでしょうね。もしかして、エクセルは高くて買えないから無料のPythonということなんでしょうか。それなら無償版Visual Studio使えばいいわけで、とにかくこれ以上時間を無駄にするのも意味がないのでVBAでコードを書いてみました。

ヤフーファイナンスは、スクレイピングを禁止しているので株式投資メモ さんをデータソースにしました。 

 

これは本当に適当なテストハーネスです。 stocksテーブルをMySQLで構築しておいて、SELECT instrument_id FROM stocks; みたいな感じでADOでループさせてもいいし、CSVファイルに4000銘柄持っておいてループさせてもいいし、自分のニーズに合わせて記述すればいいでしょう。FileStreamクラスとGetHTMLTextは別記事を参照のこと。

Public Sub foo()

    DownloadStock "1801", "2016", "2019"

End Sub 

 

ダウンロードしてCSVファイルに書き出すモジュールです。 ダウンロードした4本値をMySQLに直接書き出してもいいし、ワークシートに書き出してもいいでしょう。 

Public Sub DownloadStock(pStockID As String, pYearFrom As String, pYearTo As String)

    Dim sURL    As String
    Dim buff    As String
    Dim iPos1   As Long
    Dim iPos2   As Long
    Dim oPrices As prices
    Dim sYear   As String
    'Dim oRec As Dictionary

    Dim oStream As Stream
    Dim oFile   As FileStream
  
    ' ---- Open CSV File
    Set oFile = New FileStream
    oFile.OpenWriteStream pStockID & ".csv"
    
    ' ---- ループ: pYearFrom - pYearTo
    sYear = pYearFrom
    Do While True

        sURL = "https://kabuoji3.com/stock/" & pStockID & "/" & sYear & "/"
        buff = modUtil.GetHTMLText(sURL)

        Set oPrices = New prices

        ' ---- レコードトップまで読み飛ばす
        'Set oRec = New Dictionary
        Set oStream = New Stream
        oStream.Buffer = buff
        Do Until oStream.EOF
            buff = oStream.ReadLine()
            If InStr(buff, "<th>終値調整") > 0 Then
                Exit Do
            End If
        Loop
  
        ' ---- レコードを読み込む
        Do Until oStream.EOF
            buff = oStream.ReadLine()
            If InStr(buff, "<td>") > 0 Then
                oPrices.timestamp = parseString(buff, "<td>", "</td>")
                oPrices.openPx = parseString(oStream.ReadLine(), "<td>", "</td>")
                oPrices.highPx = parseString(oStream.ReadLine(), "<td>", "</td>")
                oPrices.lowPx = parseString(oStream.ReadLine(), "<td>", "</td>")
                oPrices.closePx = parseString(oStream.ReadLine(), "<td>", "</td>")
                oPrices.volume = parseString(oStream.ReadLine(), "<td>", "</td>")
                oPrices.fixedPx = parseString(oStream.ReadLine(), "<td>", "</td>")

                'oRec.Add pStockID & ":" & oPrices.timestamp, oPrices

                With oPrices
                    oFile.WriteStream .timestamp & "," & .openPx & "," & .highPx & "," _
                            & .lowPx & "," & .closePx & "," & .volume & "," & .fixedPx
                End With
            End If
        Loop

        sYear = sYear + 1
        If sYear > pYearTo Then Exit Do
    Loop

    oFile.CloseStream
    Set oFile = Nothing

End Sub
  
Private Function parseString(pBuff As String, pKey1 As String, pKey2 As String) As String

    Dim iPos1   As Long
    Dim iPos2   As Long

    iPos1 = InStr(pBuff, pKey1)
    iPos2 = InStr(pBuff, pKey2)

    If iPos1 + iPos2 = 0 Then
        parseString = ""
    Else
        iPos1 = iPos1 + Len(pKey1)
        parseString = Mid(pBuff, iPos1, iPos2 - iPos1)
    End If

End Function 

 

クラス:prices

Option Explicit

Public timestamp    As String
Public openPx       As Long
Public highPx       As Long
Public lowPx        As Long
Public closePx      As Long
Public volume       As Long
Public fixedPx      As Long

 

クラス:Stream  

Option Explicit

Private m_sBlock        As String
Private m_iCurrentPos   As Long
  
' ---------------------------------------------
' 文字列ブロックから1行ずつ読む
' ---------------------------------------------
Public Property Let Buffer(ByVal pBuffer As String)
    m_sBlock = pBuffer
    m_iCurrentPos = 1
End Property

Public Function ReadLine() As String

    Dim iPos    As Long
    Dim buff    As String
    Dim iLen    As Long
  
    iPos = InStr(m_iCurrentPos, m_sBlock, vbLf)
    If iPos > 0 Then
        buff = Mid(m_sBlock, m_iCurrentPos, iPos - m_iCurrentPos)
        m_iCurrentPos = iPos + 1
        ReadLine = buff
        Exit Function
    End If

    iLen = Len(m_sBlock)
    If m_iCurrentPos <= iLen Then
        ReadLine = Mid(m_sBlock, m_iCurrentPos, iLen - m_iCurrentPos + 1)
        m_iCurrentPos = iLen
    End If

End Function

Public Function EOF() As Boolean

    If m_iCurrentPos < Len(m_sBlock) Then
        EOF = False
    Else
        EOF = True
    End If

End Function
  

 

余談:楽天マーケットスピードIIをデータソースにすることも可能ですが、VBAのSendKeysもScript HostのSendKeysも安定しません。4本値以外にもアナリティクスが取れるので何とか実現したいものです。EnumerateChildWindowsでウィンドウクラスを拾って・・・とも思いましたが、コールバック関数のアドレスなんて最低でもVB6じゃないと無理だし、あきらめました。っていうか、楽天APIを公開してくれてもよさそうなんですけどね。