之四--使用 selenium 抓取和分析股票數據

本文爲系列文章 "從入門到勸退" 第四篇,同時也可做爲上一篇
puppeteer應用
的後續。css

本篇讀者對象:python初級用戶,想學習爬蟲或數據抓取的同窗。想了解 selinum 和 beautifulsoup 使用的用戶html

背景介紹:

python 長於數據處理,有一些很是優秀的庫如numpy,pandas,那搞個例子實驗一下,本人對經濟方面有些興趣,因而就拿股票行情數據分析下,經過對歷史數據的統計分析,看可否得出一家上市公司的哪些指標是決定其股票走勢的最大影響因子。前端

那麼數據從哪裏來,從網上抓唄,因而對比了騰訊股票頻道同花順和東方財富上數據獲取的便利性,選擇了同花順的數據源,經過selinum 請求獲取數據,經過beautifalsoup 分析頁面的dom獲取想要的字段,那愉快的開始吧python

數據獲取流程

step1:獲取全部股票的分頁列表,提取每一行中的股票代碼和股票中文名這兩基礎信息。
step2:有些指標在列表中沒有,因而再去請求每支股票的公司詳情頁,提取 主營業務,地區,總市值,流動市值,市盈率,市淨率。以上信息入庫,造成一個公司基礎信息表
step3:獲取每支股票的季報信息,做季報表入庫。
step4:獲取每支股票的周線數據,作周漲跌表入庫。考慮日線數據波動更具偶爾性和不缺行沒有選擇每日漲跌數據入庫,若是用月線入庫時間跨度又太長web

代碼分析

對應上面數據獲取流程的四個步驟,如下分爲四個代碼塊說明ajax

列表數據獲取與分析

分析的就是這個連接的數據 上市公司列表chrome

import time
import re
from selenium import webdriver
from bs4 import BeautifulSoup
from lwy.stock.dao.company import Company

#分別是上證A,深證A和深圳中小板
SHA = "http://q.10jqka.com.cn/index/index/board/hs/field/zdf/order/desc/page/{0}/ajax/1/"
SZA = "http://q.10jqka.com.cn/index/index/board/ss/field/zdf/order/desc/page/{0}/ajax/1/"
SZZX = "http://q.10jqka.com.cn/index/index/board/zxb/field/zdf/order/desc/page/{0}/ajax/1/"
#組合獲取,返回全部的股票數據
def getAllStock():
    #pageOne(SZA, 71)
    #pageOne(SZA, 24)
    pageOne(SZZX, 1)

#循環按頁獲取數據
def pageOne(url,pagenum):
    driver = webdriver.Chrome("./lib/chromedriver.exe")

    detail_links = []
    for page in range(5,pagenum):
        print("now pagenum is :",page)
        driver.get(url.format(page))
        detail_links = anaList(driver.page_source)
        time.sleep(15)
        #break #先只搞一頁
        #循環列表鏈接,得到全部的公司詳情並更新
        #for link in detail_links:
        #    _snatchDetail(driver,link)

    driver.quit()

#使用bs 分析獲取的htmlstr
def anaList(htmlstr):
    bf = BeautifulSoup(htmlstr,"html.parser")
    trs = bf.select("tbody tr")
    #公司詳情信息連接
    comp_links = []
    #trs = bf.find("tbody").children
    for tr in trs:
        #總共14個元素
        astock = {}
        ind = 1
        #print("tr:",tr)
        tds = tr.find_all("td")
        for td in tds:
            if ind == 2: #gp代碼
                astock["stock_code"] = td.text
                comp_links.append("http://stockpage.10jqka.com.cn/{0}/company/".format(td.text))
            elif ind == 3: #中文名
                astock["company_name"] = td.text
                break
            ind += 1
    
        #print(astock)
        Company().add(astock)

    return comp_links

以上出現了 selinum 和 bf 的初級使用,比較簡單就不說了。整個過程不自動化,須要一邊獲取數據一遍觀察分析,發現數據不正確或者有異常就立刻中止程序,而後修改參數繼續。數據庫

公司詳情數據獲取

#查詢全部沒有填充詳情的,繼續填
def fillExtend():
    stocks = Company().GetUnFill()
    driver = webdriver.Chrome("./lib/chromedriver.exe")
    url = "http://stockpage.10jqka.com.cn/{0}/company/"
    for code in stocks:
        _snatchDetail(driver,url.format(code))

#從詳情頁面抓取補充信息
def _snatchDetail(driver,link):
    m = re.search(r"\d{6}",link)
    comp = {"code":m.group()}
    driver.get(link)
    try:
        driver.switch_to.frame("dataifm")
    except Exception as ex:
        print("cannot found frame:",comp)
        return
    htmlb = driver.find_element_by_css_selector(".m_tab_content2").get_attribute("innerHTML")
    bf = BeautifulSoup(htmlb,"html.parser")
    strongs = bf.select("tr>td>span")
    comp["main_yewu"] = strongs[0].text
    comp["location"] = strongs[-1].text

    driver.switch_to.parent_frame()
    driver.switch_to.frame("ifm")
    time.sleep(3)
    htmla = driver.find_element_by_css_selector("ul.new_trading").get_attribute("innerHTML")
    bf = BeautifulSoup(htmla,"html.parser")
    _getvalues(bf,comp)
    #print("list.py comp:",comp)
    Company().update(comp)

    time.sleep(10)

def _getvalues(bf,comp):
    strongs = bf.select("li span strong")
    comp["total_value"] = strongs[7].text
    comp["flut_value"] = strongs[10].text 
    comp["clean_value"] = strongs[8].text 
    profit = strongs[11].text
    if profit == "虧損":
        profit = -1.0
    comp["profit_value"] = profit

須要留意一下的是這兩行
driver.switch_to.parent_frame()
driver.switch_to.frame("ifm")
進行元素查找時須要留意頁面是否有iframe,若是有應先將driver跳至對應的frame, 思路與前端使用document 一致json

周線數據獲取

#周線數據獲取
import urllib.request
import time
import re
import os
import json
from lwy.stock.dao.company import Company
from lwy.stock.dao.weekline import WeekLine

def GetWeekLine():
    codes = Company().PageCode("600501",1000)

    url = "http://d.10jqka.com.cn/v6/line/hs_{0}/11/all.js"
    header = [("Referer", "http://stockpage.10jqka.com.cn/HQ_v4.html"),
        ("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.80 Safari/537.36")]
    for code in codes:
        print("code:",url.format(code))
        opener = urllib.request.build_opener()
        opener.addheaders = header
        
        with opener.open(url.format(code)) as resp:
            content = resp.read().decode()
        m = re.search(r"{.*}",content)
        if m is None:
            print("not found:",code)
        else:
            with open("./weeks/{0}.json".format(code),"w",encoding="utf-8") as wfile:
                wfile.write(m.group())

        time.sleep(10)


#從json文件中分析周線
def ana_weekline():
    #遍歷文件目錄
    files = os.listdir("./weeks")
    for file in files:
        fname = "./weeks/"+file
        if os.path.isfile(fname):
            bsname = file[0:6]
            with open(fname,encoding="utf-8") as rfile:
                content = rfile.read()
                _withJSON(bsname,json.loads(content))
                #成功以後須要移出json 文件到另外的目錄
                #os.rename(file,file+"_old")
            #break #分析一個即中止
    pass

def WeekTest():
    with open("./weeks/002774.json",encoding="utf-8") as rfile:
        content = rfile.read()
        _withJSON("002774",json.loads(content))

def _withJSON(scode,jdata):
    dates = jdata["dates"].split(',')
    prices = jdata["price"].split(",")
    myears = jdata["sortYear"]
    #最多容許4年,年份和周的數據實例以下 [[2017,40],[2018,51]]
    if len(myears)>4: #作多隻獲取四年
        myears = myears[-4:]
    preyear = [] #年份頭,該數組保存最近4年的全部周線的年份頭
    for item in myears:
        y = item[0]
        num = item[1]
        preyear.extend( [y for i in range(num)])
    #price數據和日誌數據都要從最尾部開始循環
    #print("preyear:",preyear)
    week = len(preyear)
    while week >0:
        ind_week = -1*week
        #形如如下4個值組合成一個週數據 低,開,高,收
        ind_price = -4*week
        #如下分別獲得3條數據,開,收,波動  和周全名
        kai = float(prices[ind_price])+float(prices[ind_price+1])
        shou = float(prices[ind_price]) +float(prices[ind_price+3])
        wave = (shou-kai)*100/kai   #波動以百分數計
        wfull = str(preyear[ind_week]) + dates[ind_week]
        week -= 1
        #注意wave是波動,而漲跌應該是和昨天的數據比,而不是今天,wave彷佛沒有意義
        #print("{0}: 開--{1},收--{2},波動--{3:.2f}".format(wfull,kai,shou,wave))
        #順序:stock_code,week,start_value,end_value,wave_value
        wl = (scode,wfull,kai,shou,wave)

        WeekLine().AddOne(wl)

周線數據實際上是經過請求一個js 而後返回的json數據,並保存。而後再一個個文件讀取和分析segmentfault

季報數據獲取

import time
from selenium import webdriver
from bs4 import BeautifulSoup
from lwy.stock.dao.company import Company
from lwy.stock.dao.reports import SeasonReport

driver = webdriver.Chrome("./lib/chromedriver.exe") 

#對外公開接口,爬取季度報告
def spideSeason():
#按批次獲取股票代號,而後循環
    codes = Company().PageCode("002114",1000)
    for code in codes:
        print("now get code is :",code)
        content = _fromHttp(code)
        if content == "":
            continue
        _anaReport(content,code)
        time.sleep(10)
    

def _anaReport(content, code):
    bf = BeautifulSoup(content,"html.parser")
    divs = bf.find("div",id="data-info").find_next_sibling().select("div.td_w")
    seasons = []
    #最多16 個季度,若是不夠則以數據表中自己季度個數爲準
    sealen = 0
    for div in divs:
        if sealen >=16:
            break
        seasons.append(div.text)
        sealen+=1
    
    keymap = {"3":"total_profit","4":"profit_ratio","5":"total_income","6":"income_ratio","9":"clean_ratio",10:"debt_ratio"}
    trs = bf.select("table.tbody > tbody > tr")
    reports = [ {"season":x} for x in seasons ]
    #print("reports:",reports)
    for ind,keyname in keymap.items():
        #索引對應說明 3:扣非淨利潤,4:扣非淨利潤增加率,5總營收,6營收增加率,9淨資產收益率,10負債率
        tds = trs[int(ind)].find_all("td")
        for tdindex in range(0,sealen):
            text = tds[tdindex].text
            if "%" in text:
                text = text.replace("%","")
            elif "億" in text:
                text = text.replace("億","")
            elif "萬" in text:
                f = float(text.replace("萬",""))
                text = "{0:.4f}".format(f/10000.0)
            reports[tdindex][keyname] = text
    for r in reports:
        r["stock_code"] = code
        #淨利潤或者營業總收入同時爲空不作記錄
        if r["total_income"] == "" or r["total_income"] == "":
            continue
        #print(r)
        SeasonReport().add(r)

def _fromHttp(scode):
    global driver
    driver.get("http://stockpage.10jqka.com.cn/{0}/finance/#finance".format(scode))
    time.sleep(3)
    try:
        driver.switch_to_frame("dataifm")
    except:
        return ""
    #找到季度報告的li,並點擊
    tab3 = driver.find_element_by_css_selector("ul.tabDataTab").find_element_by_link_text("按單季度")
    tab3.click()
    time.sleep(1)
    content = driver.find_element_by_css_selector("div.data_tbody").get_attribute("innerHTML")
    with open("./reports/{0}.html".format(scode),"w",encoding="utf-8") as wfile:
        wfile.write(content)

    return content

季報數據的初始啓動函數固定寫了個股票代號,這是經過數據庫查詢獲得的,由於數據獲取基本都是按照股票代號遞增處理。
財務報告數據按報告期,季報和年報分多個tab ,此處經過tab3 = driver.find_element_by_css_selector("ul.tabDataTab").find_element_by_link_text("按單季度")
tab3.click()
time.sleep(1)
進行切換,sleep 1毫秒是我的習慣,作了操做總喜歡稍等,沒有追究是否有意義

幾點想法

1:控制請求頻率。同花順頁面請求應該是有頻率限制的,請求過快會跳至以下的頁面 [http://stockpage.10jqka.com.cn/] 文中頻率幾乎是一個臨界值了,多了就會自動跳轉。

2:分步驟分階段獲取。數據獲取自己是逐步完善,數據來源看似有統一規格實際並非,好比季報中的淨利潤,本來你設計數據類型是浮點,然而文中卻有個別的 '-' ,凡此種種均可能致使數據丟失,異常或錄入錯誤。期待一次性自動化獲取完並不現實,而一旦錯誤,就要全盤的從新獲取,浪費大量請求,還可能被屏蔽。因此最好的,一層數據獲取,檢查確認,再繼續獲取下一層,如此分步驟,並日志記錄分析到那一條,再次分析則可從異常處開始

3: 遇到坑,可繞着走。這也許不是積極態度,但有時候卻頗有用,填坑太費時間了。學習一項內容,不可能一下把它全面搞清楚,容易有盲點或者一時找不到解決辦法,此時稍做停頓,考慮下必定要這麼作嗎,還有沒有其它辦法嗎

數據清理

專業的叫法也許叫數據清洗
抓取的數據有少許是沒有參考價值,爲減小其負面影響需過濾或者補充例如:
剛上市或者是上市時間小於一年
季報數據不全或季報內收入和盈利信息是 "-"
長時間停牌的
僅選取公司地址爲爲大城市,特別是剔除公司總部在三四線小城(此類公司管理能力,利益糾葛,內幕交易等各種非經營因素影響更大)
......

共得約42w條周波動數據,4w季報數據,2k+上市公司基礎數據 (thx 會找我麻煩麼,好怕怕)

數據分析,已勸退

也許經過專門的金融數據接口能夠得到上述數據,沒有仔細研究過,但本文做爲 selinum 和 beautifulsoup (是否是很像beautifulsoap 美麗的肥皂,撿?-?)的使用實例,已經有點意思了,然而獲取數據就是爲了分析,而且個人初衷是但願依據過去上市公司的季度經營數據和周漲跌,來預測將來股票的漲跌。

然依據我這仍是十幾年前的高等數學知識,並持續的退化與遺忘,已經難以找到計算模型去擬合過去和預測將來,若是哪位同窗有相關的經驗,能夠指明個方向,若是能具體給出相似的例子(博客地址也可)那就更好了。
歡迎私信或在評論處回覆,感謝!

相關文章
相關標籤/搜索