本文为系列文章 "从入门到劝退" 第四篇,同时也可做为上一篇
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 美丽的肥皂,捡?-?)的使用实例,已经有点意思了,然而获取数据就是为了分析,而且个人初衷是但愿依据过去上市公司的季度经营数据和周涨跌,来预测将来股票的涨跌。
然依据我这仍是十几年前的高等数学知识,并持续的退化与遗忘,已经难以找到计算模型去拟合过去和预测将来,若是哪位同窗有相关的经验,能够指明个方向,若是能具体给出相似的例子(博客地址也可)那就更好了。
欢迎私信或在评论处回复,感谢!