本文为系列文章 "从入门到劝退" 第四篇,同时也可作为上一篇
puppeteer应用
的后续。
本篇读者对象:python初级用户,想学习爬虫或数据抓取的同学。想了解 selinum 和 beautifulsoup 使用的用户
背景介绍:
python 长于数据处理,有一些非常优秀的库如numpy,pandas,那搞个例子实验一下,本人对经济方面有些兴趣,于是就拿股票行情数据分析下,通过对历史数据的统计分析,看能否得出一家上市公司的哪些指标是决定其股票走势的最大影响因子。
那么数据从哪里来,从网上抓呗,于是对比了腾讯股票频道同花顺和东方财富上数据获取的便利性,选择了同花顺的数据源,通过selinum 请求获取数据,通过beautifalsoup 分析页面的dom获取想要的字段,那愉快的开始吧
数据获取流程
step1:获取所有股票的分页列表,提取每一行中的股票代码和股票中文名这两基础信息。
step2:有些指标在列表中没有,于是再去请求每支股票的公司详情页,提取 主营业务,地区,总市值,流动市值,市盈率,市净率。以上信息入库,形成一个公司基础信息表
step3:获取每支股票的季报信息,作季报表入库。
step4:获取每支股票的周线数据,做周涨跌表入库。考虑日线数据波动更具偶尔性和不缺行没有选择每日涨跌数据入库,如果用月线入库时间跨度又太长
代码分析
对应上面数据获取流程的四个步骤,以下分为四个代码块说明
列表数据获取与分析
分析的就是这个链接的数据 上市公司列表
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 一致
周线数据获取
#周线数据获取
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数据,并保存。然后再一个个文件读取和分析
季报数据获取
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 美丽的肥皂,捡?-?)的使用实例,已经有点意思了,然而获取数据就是为了分析,并且我的初衷是希望依据过去上市公司的季度经营数据和周涨跌,来预测未来股票的涨跌。
然依据我这还是十几年前的高等数学知识,并持续的退化与遗忘,已经难以找到计算模型去拟合过去和预测未来,如果哪位同学有相关的经验,可以指明个方向,如果能具体给出类似的例子(博客地址也可)那就更好了。
欢迎私信或在评论处回复,感谢!