一、用cookie池模拟登录
在网络请求交互中,为了维持用户的登录状态,引入了cookie的概念。当用户第一次登录某个网站时,网站服务器会返回维持登录状态需要用到的信息,这些信息就称为cookie。浏览器会将cookie信息保存在本地计算机中,再次对同一网站发起请求时就会携带上cookie信息,服务器从中可以分析判断出用户的登录状态。
服务器中的资源有些不需要登录就能获取,有些则需要登录才能获取,如果在爬虫程序中携带正确的cookie信息,就可以爬取那些需要登录才能获取的数据了。
1、用浏览器获取cookie信息
代码文件:用浏览器获取cookie信息.py
第一次登录一个网页后,浏览器会从响应头的set-cookie字段中读取cookie值并保存起来。下次访问该网页时,浏览器就会携带cookie值发起请求,服务器从cookie值中得到用户登录信息,就会直接返回用户登录之后的页面。下面以人人网为例讲解如何获取cookie值。
在谷歌浏览器中打开人人网(http://www.renren.com/),输入账号和密码,登录成功后通过开发者工具对数据进行抓包,即在开发者工具的“Network”选项卡下刷新当前页面后选中第一个数据包,在“Headers”选项卡下的“Request Headers”中查看Cookie字段,该字段的值就是发起请求时携带的cookie值,如下图所示。
在爬虫程序中使用requests模块的get()函数发起请求时,携带cookie值的方式有两种,下面分别介绍。
第一种方式是在get()函数的参数headers中直接携带cookie值的字符串。
演示代码如下:
1 import requests2 url = 'http://www.renren.com/974388972/newsfeed/photo'3 cookie = 'anonymid=kbbumh2lxrmpf1; depovince=GW; _r01_=1; JSESSIONID=abcGWVhbBr19FhENjsNkx; ick_login=989861b2-b103-4dec-acae-a997d02bbaa6; taihe_bi_sdk_uid=f5185c56d440f6d9816cbd80ee1c01e3; taihe_bi_sdk_session=b57a76d842fee2d91ce5701999cb1dbe; XNESSESSIONID=abcoL9H_VgmAddSCksNkx; ick=c1409cb0-0813-44ec-8834-729af8f535cd; t=d06e12b9149594f6fba8cc62f9e8f3e32; societyguester=d06e12b9149594f6fba8cc62f9e8f3e32; id=974598592; xnsid=58ab78d8; WebOnLineNotice_974598592=1; jebecookies=3b9d3b87-1f8b-4867-a20b-b183cc3b36c4|||||; ver=7.0; loginfrom=null; wp_fold=0'4 headers = {'Cookie': cookie, 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36'} # 在headers中携带cookie值5 response = requests.get(url=url, headers=headers).text # 获取页面源代码6 print(response)
第二种方式是将获取到的cookie值的字符串处理成字典,作为get()函数的参数cookies携带上。
演示代码如下:
1 import requests2 url = 'http://www.renren.com/974388972/newsfeed/photo'3 cookie = 'anonymid=kbbumh2lxrmpf1; depovince=GW; _r01_=1; JSESSIONID=abcGWVhbBr19FhENjsNkx; ick_login=989861b2-b103-4dec-acae-a997d02bbaa6; taihe_bi_sdk_uid=f5185c56d440f6d9816cbd80ee1c01e3; taihe_bi_sdk_session=b57a76d842fee2d91ce5701999cb1dbe; XNESSESSIONID=abcoL9H_VgmAddSCksNkx; ick=c1409cb0-0813-44ec-8834-729af8f535cd; t=d06e12b9149594f6fba8cc62f9e8f3e32; societyguester=d06e12b9149594f6fba8cc62f9e8f3e32; id=974598592; xnsid=58ab78d8; WebOnLineNotice_974598592=1; jebecookies=3b9d3b87-1f8b-4867-a20b-b183cc3b36c4|||||; ver=7.0; loginfrom=null; wp_fold=0'4 headers = {'Cookie': cookie, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36'}5 cookie_dict = {}6 for i in cookie.split(';'): # 将cookie值的字符串格式化为字典7 cookie_keys = i.split('=')[0]8 cookie_value = i.split('=')[1]9 cookie_dict[cookie_keys] = cookie_value10 response = requests.get(url=url, headers=headers, cookies=cookie_dict).text # 将cookie字典作为参数cookies的值,获取页面源代码11 print(response)
2、自动记录cookie信息
代码文件:自动记录cookie信息.py
上面介绍用浏览器获取cookie信息的方式需要我们自己在数据包中定位cookie信息并提取出来,相对比较烦琐。
这里介绍一种自动记录登录数据的方法:先使用requests模块中的Session对象对一个网站发起登录请求,该对象会记录此次登录用到的cookie信息;再次使用该对象对同一网站的其他页面发起请求时,就会直接使用记录下的cookie信息,不再需要额外携带cookie信息参数了。
演示代码如下:
1 import requests2 s = requests.Session() # 实例化一个Session对象3 headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36'} # 验证浏览器身份的信息4 data = {'email': '15729560000', 'password': '123456'} # 用于登录网站的账号/密码信息5 response=s.post(url='http://www.renren.com/ajaxLogin/login" /> 登录成功后,在“Network”选项卡下的“Name”窗格中产生了很多新的数据包,其中就有登录表单的数据包,包名中一般有“login”字样,可在“Filter”筛选器中输入关键词“login”来筛选数据包。
筛选后可在“Name”窗格中看到含有“login”字样的数据包,单击这个包,在“Headers”选项卡下的“Form Data”中可以看到登录时携带的表单信息,一般只需要账号和密码的字段名(这里为“email”和“password”),在发起post请求时携带即可,如下图所示。
二、爬取当当网销售排行榜
代码文件:案例:爬取当当网的图书销售排行榜.py
图书销售排行榜对于出版社编辑制定选题开发方向、图书销售商制定进货计划具有很高的参考价值。实体书店的图书销售排行数据采集难度较大,而电子商务网站的图书销售排行数据则具有真实性高、更新及时、容易获取等优点,是一个相当好的数据来源。
步骤1:先确定需要爬取数据的网址。在谷歌浏览器中打开当当网,进入“图书畅销榜”,网址为http://bang.dangdang.com/books/bestsellers/01.00.00.00.00.00-24hours-0-0-1-1。为了增强数据的时效性,选择查看近30日的排行榜数据,发现网址随之变为http://bang.dangdang.com/books/bestsellers/01.00.00.00.00.00-recent30-0-0-1-1,该网址显示的是排行榜的第1页内容。
通过观察页面底部的翻页链接可以发现,排行榜共有25页,单击第2页的链接,可以发现网址的最后一个数字变为2,依此类推。根据这一规律,如果要爬取所有页码,只需使用for语句构造循环,按页码依次进行网址的拼接并发起请求即可。
演示代码如下:
1 import requests2 import pandas as pd3 from bs4 import BeautifulSoup4 headers = {'User-Agent':' Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'} # 模拟浏览器的身份验证信息5 for i in range(1, 26):6 url = f'http://bang.dangdang.com/books/bestsellers/01.00.00.00.00.00-recent30-0-0-1-{i}' # 循环生成每一页的网址
步骤2:确定数据是否是动态加载的。打开开发者工具,切换到“Network”选项卡,然后单击“All”按钮,按【F5】键刷新页面,在“Name”窗格中找到主页面的数据包并单击(通常为第一个数据包)。
在“Name”窗格的右侧切换至“Response”选项卡,并在网页源代码的任意处单击,按快捷键【Ctrl+F】调出局部搜索框,输入排行第一的图书名称,如“你当像鸟飞往你的山”,搜索后发现在“Response”选项卡下的网页源代码中有该关键词。这说明数据不是动态加载的,可以用响应对象的text属性获取网页源代码,再从源代码中提取数据。
在步骤1的for语句构造的循环内部继续添加如下代码:
1 response = requests.get(url=url, headers=headers, timeout=10) # 对25页的不同网址循环发起请求,设置超时等待为10秒2 html_content = response.text # 获取网页源代码3 soup = BeautifulSoup(html_content, 'lxml') # 将网页源代码实例化为BeautifulSoup对象4 parse_html(soup) # 调用自定义函数提取BeautifulSoup对象中的数据,该函数的具体代码后面再来编写5 print(f'第{i}页爬取完毕')
如果用局部搜索找不到关键词,则说明数据是动态加载的,需要携带动态参数发送请求,再使用json()函数提取数据。
步骤3:分析要提取的数据位于哪些标签中。利用开发者工具可看到要提取的排行榜数据位于class属性值为bang_list的
标签下的多个- 标签中,如下图所示。
每个
- 标签中存储着一本图书的详细数据,包括排名、书名、作者、出版时间、出版社、价格等。还需要继续利用开发者工具更细致地剖析源代码,定位这些数据所在的标签。具体方法和前面类似,这里就不展开讲解了,留给读者自己练习。
步骤4:编写提取数据的代码。先创建一个字典data_info,然后根据上一步的分析结果编写自定义函数parse_html(),从BeautifulSoup对象中提取数据并添加到字典中。
演示代码如下:
1 data_info = {'图书排名': [], '图书名称': [], '图书作者': [], '图书出版时间': [], '图书出版社': [], '图书价格': []} # 新建一个空字典2 def parse_html(soup): # 解析每一个BeautifulSoup对象3 li_list = soup.select('.bang_list li') # 通过层级选择器定位class属性值为bang_list的标签下的所有- 标签4 for li in li_list: # 将从每一个
- 标签中解析到的数据添加到字典data_info相应键的对应列表中5 data_info['图书排名'].append(li.select('.list_num ')[0].text.replace('.', ''))6 data_info['图书名称'].append(li.select('.name a')[0].text)7 data_info['图书作者'].append(li.select('.publisher_info ')[0].select('a')[0].text)8 data_info['图书出版时间'].append(li.select('.publisher_info span')[0].text)9 data_info['图书出版社'].append(li.select('.publisher_info ')[1].select('a')[0].text)10 data_info['图书价格'].append(float(li.select('.price .price_n')[0].text.replace('¥', '')))
第7、8、9行代码使用层级选择器通过class属性值publisher_info进行标签定位,因为有两个标签的class属性都是这个值,所以先做列表切片再使用select()函数进行定位,才能获得准确的数据。总体来说,如果多个标签的属性值一样,可以多次使用select()函数进行定位,这样获得的数据更准确。
第10行代码是为后面的数据清洗做准备,将“¥”替换为空值(即删除),再用float()函数将字符串转换为浮点型数字,以方便比较数据大小。
需要注意的是,在最终的代码文件中,定义parse_html()函数的代码需位于调用该函数的代码之前。步骤5:缺失值和重复值的处理。获取了每一页的数据并存储到字典data_info中后,还需要进行数据清洗。先用pandas模块将字典转换为DataFrame对象格式,再判断缺失值和重复值。
演示代码如下:
1 book_info = pd.DataFrame(data_info)2 print(book_info.isnull()) # 缺失值判断3 print(book_info.duplicated()) # 重复值判断
第3行代码中的duplicated()函数用于判断是否有重复行,如果有,则返回True。运行后,结果全为False,说明代码没有问题,获取到的数据也没有重复值和缺失值,接着进行异常值的处理。
步骤6:异常值的处理。假定本案例只研究价格在100元以下的图书,所以需要将价格高于100元的图书删除。
演示代码如下:
1 book_info['图书价格'][book_info['图书价格'] > 100] = None # 将大于100的图书价格替换为空值2 book_info = book_info.dropna() # 删除有空值的一行数据
步骤7:保存爬取的数据。最后,将处理好的数据保存起来,这里存储为csv文件。
演示代码如下:
1 book_info.to_csv('当当网图书销售排行.csv', encoding='utf-8', index=False)
三、CSDN数据采集
通过python实现csdn页面的内容采集是相对来说比较容易的,因为csdn不需要登陆,不需要cookie,也不需要设置header。
本案例使用python实现csdn文章数据采集,获取我的博客下每篇文章的链接、标题、阅读数目。
需要安装BeautifulSoup包(点击下载)
python3.6下:
#coding:utf-8#本实例用于获取指定用户csdn的文章名称、连接、阅读数目import urllibimport refrom bs4 import BeautifulSoup#csdn不需要登陆,也不需要cookie,也不需要设置headerprint('=======================csdn数据挖掘==========================')urlstr="https://blog.csdn.net/qq_35029061?viewmode=contents"host = "https://blog.csdn.net/qq_35029061" #根目录alllink=[urlstr] #所有需要遍历的网址data={}def getdata(html,reg): #从字符串中安装正则表达式获取值 pattern = re.compile(reg) items = re.findall(pattern, html) for item in items: urlpath = urllib.parse.urljoin(urlstr,item[0]) #将相对地址,转化为绝对地址 if not hasattr(object, urlpath): data[urlpath] = item print(urlpath,' ', end=' ') #python3中end表示结尾符,这里不换行 print(item[2], ' ', end=' ') print(item[1])#根据一个网址获取相关连接并添加到集合中def getlink(url,html): soup = BeautifulSoup(html,'html5lib') #使用html5lib解析,所以需要提前安装好html5lib包 for tag in soup.find_all('a'): #从文档中找到所有标签的内容 link = tag.get('href') newurl = urllib.parse.urljoin(url, link) #在指定网址中的连接的绝对连接 if host not in newurl: # 如果是站外连接,则放弃 continue if newurl in alllink: #不添加已经存在的网址 continue if not "https://blog.csdn.net/qq_35029061/article/list" in newurl: #自定义添加一些链接限制 continue # if not "https://blog.csdn.net/qq_35029061/category_10969287" in newurl: #必须是python类目下的列表 # continue # if not "https://blog.csdn.net/qq_35029061/category_11852875" in newurl: # 必须是K8s类目下的列表 # continue alllink.append(newurl) #将地址添加到链接集合中#根据一个网址,获取该网址中符合指定正则表达式的内容def craw(url): try: request = urllib.request.Request(url) #创建一个请求 response = urllib.request.urlopen(request) #获取响应 html = str(response.read(),'utf-8') #读取返回html源码,,python2里面读取的是字节数组 # reg = r'"link_title">\r\n(.*?)\n.*?' #只匹配文章地址和名称 reg = r'"link_title">\r\n (.*?) \r\n.*?[\s\S]*?阅读\((.*?)\)' # 匹配地址、名称、阅读数目 getdata(html,reg) getlink(url,html) except urllib.error.URLError as e: if hasattr(e,"code"): print(e.code) if hasattr(e,"reason"): print(e.reason)for url in alllink: craw(url)
python2.7下:
#coding:utf-8#本实例用于获取指定用户csdn的文章名称、连接、阅读数目import urllib2import refrom bs4 import BeautifulSoup#csdn不需要登陆,也不需要cookie,也不需要设置headerprint('=======================csdn数据挖掘==========================')urlstr="http://blog.csdn.net/luanpeng825485697?viewmode=contents"host = "http://blog.csdn.net/luanpeng825485697" #根目录alllink=[urlstr] #所有需要遍历的网址data={}def getdata(html,reg): #从字符串中安装正则表达式获取值 pattern = re.compile(reg) items = re.findall(pattern, html) for item in items: urlpath = urllib2.urlparse.urljoin(urlstr,item[0]) #将相对地址,转化为绝对地址 if not hasattr(object, urlpath): data[urlpath] = item print urlpath,' ', #print最后有个逗号,表示输出不换行 print item[2], ' ', print item[1]#根据一个网址获取相关连接并添加到集合中def getlink(url,html): soup = BeautifulSoup(html,'html.parser') #使用html5lib解析,所以需要提前安装好html5lib包 for tag in soup.find_all('a'): #从文档中找到所有标签的内容 link = tag.get('href') newurl = urllib2.urlparse.urljoin(url, link) #在指定网址中的连接的绝对连接 if host not in newurl: # 如果是站外连接,则放弃 continue if newurl in alllink: #不添加已经存在的网址 continue if not "http://blog.csdn.net/luanpeng825485697/article/list" in newurl: #自定义添加一些链接限制 continue alllink.append(newurl) #将地址添加到链接集合中#根据一个网址,获取该网址中符合指定正则表达式的内容def craw(url): try: request = urllib2.Request(url) #创建一个请求 response = urllib2.urlopen(request) #获取响应 html = response.read() #读取返回html源码 # reg = r'"link_title">\r\n(.*?)\n.*?' #只匹配文章地址和名称 reg = r'"link_title">\r\n (.*?) \r\n.*?[\s\S]*?阅读\((.*?)\)' # 匹配地址、名称、阅读数目 getdata(html,reg) getlink(url,html) except urllib2.URLError, e: if hasattr(e,"code"): print e.code if hasattr(e,"reason"): print e.reasonfor url in alllink: craw(url)
四、糗事百科数据采集
通过python实现糗事百科页面的内容采集是相对来说比较容易的,因为糗事百科不需要登陆,不需要cookie,不过需要设置http的MIME头,模拟浏览器访问才能正常请求
本案例使用python实现糗事百科数据采集,获取糗事百科热门的文章内容和好评数量。
需要安装BeautifulSoup包(点击下载)
python2.7下:
#coding:utf-8#本实例用于获取糗事百科热门的文章内容和好评数量。import urllib2import refrom bs4 import BeautifulSoup#糗事百科需要设置MIME头才能正常请求,不需要登陆,也不需要cookieprint('=======================糗事百科数据挖掘==========================')urlstr="https://www.qiushibaike.com/8hr/page/%d"data={}def getdata(html): #从字符串中安装正则表达式获取值 soup = BeautifulSoup(html, 'html.parser'); alldiv = soup.find_all("div", class_="content") #内容的外部div allnum = soup.find_all("span", class_="stats-vote") #点赞数量的外部span for i in range(0,len(alldiv)): print str(alldiv[i].find_all('span')[0]).replace('','').replace('','').replace('
','\r\n').strip() #内容文字,使用string在文字里还有
时,无法打印,使用text会省略调用
print allnum[i].find_all('i')[0].string #好评数量 #根据一个网址,获取该网址中符合指定正则表达式的内容def craw(url): try: user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' headers = { 'User-Agent' : user_agent } #设置MIME头,糗事百科对这个进行了验证 request = urllib2.Request(url,headers = headers) #创建一个请求 response = urllib2.urlopen(request) #获取响应 html = response.read() #读取返回html源码 getdata(html) except urllib2.URLError, e: if hasattr(e,"code"): print e.code if hasattr(e,"reason"): print e.reasonfor i in range(1,14): url = urlstr % i print(url) craw(url)
python3.6下:
#coding:utf-8#本实例用于获取糗事百科热门的文章内容和好评数量。import urllibfrom bs4 import BeautifulSoup#糗事百科需要设置MIME头才能正常请求,不需要登陆,也不需要cookieprint('=======================糗事百科数据挖掘==========================')urlstr="https://www.qiushibaike.com/8hr/page/%d"data={}def getdata(html): #从字符串中安装正则表达式获取值 soup = BeautifulSoup(html, 'html.parser'); alldiv = soup.find_all("div", class_="content") #内容的外部div allnum = soup.find_all("span", class_="stats-vote") #点赞数量的外部span for i in range(0,len(alldiv)): print(str(alldiv[i].find_all('span')[0]).replace('','').replace('','').replace('
','\r\n').strip()) #内容文字,使用string在文字里还有
时,无法打印,使用text会省略调用
print(allnum[i].find_all('i')[0].string) #好评数量#根据一个网址,获取该网址中符合指定正则表达式的内容def craw(url): try: user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' headers = { 'User-Agent' : user_agent } #设置MIME头,糗事百科对这个进行了验证 request = urllib.request.Request(url,headers = headers) #创建一个请求 response = urllib.request.urlopen(request) #获取响应 html = response.read() #读取返回html源码 getdata(html) except urllib.error.URLError as e: if hasattr(e,"code"): print(e.code) if hasattr(e,"reason"): print(e.reason)for i in range(1,14): url = urlstr % i print(url) craw(url)
五、百度贴吧数据采集
通过python实现百度贴吧页面的内容采集是相对来说比较容易的,因为百度贴吧不需要登陆,不需要cookie,不需要设置http的MIME头
本案例使用python实现百度贴吧数据采集,获取百度贴吧的文章内容,楼层
百度贴吧网址比如:http://tieba.baidu.com/p/3138733512?see_lz=1&pn=1,这是一个关于NBA50大的盘点,分析一下这个地址。
http:// 代表资源传输使用http协议 tieba.baidu.com 是百度的二级域名,指向百度贴吧的服务器。 /p/3138733512 是服务器某个资源,即这个帖子的地址定位符 see_lz和pn是该URL的两个参数,分别代表了只看楼主和帖子页码,等于1表示该条件为真
所以我们可以把URL分为两部分,一部分为基础部分,一部分为参数部分。
例如,上面的URL我们划分基础部分是 http://tieba.baidu.com/p/3138733512,参数部分是 ?see_lz=1&pn=1
爬虫过程比较简单,基本还是围绕:请求、正则解析、打印存储。
注意:python3.4以后中,将urllib2、urlparse、robotparser并入了urllib模块,并且修改了urllib模块,其中包含了5个子模块,每个子模块中的常用方法如下:
python3中的库 包含了子类(python2中)urllib.error: ContentTooShortError;URLError;HTTPErrorurllib.parse: urlparse;_splitparams;urlsplit;urlunparse;urlunsplit;urljoin;urldefrag;unquote_to_bytes;unquote;parse_qs;parse_qsl;unquote_plus;quote;quote_plus;quote_from_bytes;urlencode;to_bytes;unwrap;splittype;splithost;splituser;splitpasswd;splitport等;urllib.request: urlopen; install_opener; urlretrieve; urlcleanup; request_host; build_opener; _parse_proxy; parse_keqv_list; parse_http_list; _safe_gethostbyname; ftperrors; noheaders; getproxies_environment; proxy_bypass_environment; _proxy_bypass_macosx_sysconf; Requesturllib.response: addbase; addclosehook; addinfo;addinfourl;urllib.robotparser: RobotFileParser
python2.7下:
# -*- coding:utf-8 -*-import urllibimport urllib2import re#处理页面标签类class Tool: #去除img标签,7位长空格 removeImg = re.compile('
执行结果:
十、多线程+队列+入库:根据关键字筛选爬取网页
实现思路:
多线程爬取网页信息,从一个页面为起点,爬取其包含的所有链接,并根据关键字筛选,将符合的网页入库。
- 访问首页(种子页),获取源码 html;
- 使用正则或者其他方式获取所有的绝对地址链接,存到一个 list 里面;
- 遍历 list,加入到队列中;
- 多线程从队列中取数据,一次取一个绝对地址链接,并重复上面 1-3 步。
代码:
下载地址:https://github.com/juno3550/Crawler
- MysqlTool 类:将 mysql 的初始化及操作进行封装。
get_page_message():获取当前网页的所需信息(标题、正文、包含的所有链接)。
- get_html():多线程任务函数,将包含关键字的当前页面进行入库,并将包含的所有链接放入队列中,以此循环。
import requestsimport refrom threading import Thread, Lockimport queueimport pymysqlimport tracebackimport time# 线程同步锁lock = Lock()queue = queue.Queue()# 设定需要爬取的网页数crawl_num = 20# 存储已爬取的urlresult_urls = []# 当前已爬取的网页个数current_url_count = 0# 爬取的关键字key_word = "新闻"# mysql操作封装class MysqlTool(): def __init__(self, host, port, db, user, passwd, charset="utf8"): self.host = host self.port = port self.db = db self.user = user self.passwd = passwd self.charset = charset def connect(self): '''创建数据库连接与执行对象''' try: self.conn = pymysql.connect(host=self.host, port=self.port, db=self.db, user=self.user, passwd=self.passwd, charset=self.charset) self.cursor = self.conn.cursor() except Exception as e: print(e) def close(self): '''关闭数据库连接与执行对象''' try: self.cursor.close() self.conn.close() except Exception as e: print(e) def __edit(self, sql): '''增删改查的私有方法''' try: execute_count = self.cursor.execute(sql) self.conn.commit() except Exception as e: print(e) else: return execute_count def insert(self, sql): '''插入数据''' self.connect() self.__edit(sql) self.close()# 获取URL的所需信息def get_page_message(url): try: if ".pdf" in url or ".jpg" in url or ".jpeg" in url or ".png" in url or ".apk" in url or "microsoft" in url: return r = requests.get(url, timeout=5) # 获取该URL的源码内容 page_content = r.text # 获取页面标题 page_title = re.search(r"(.+" />= crawl_num: print("【线程%d】爬取总数【%d】达到要求,任务函数结束" % (threading_no, len(result_urls))) lock.release() return else: lock.release() # 从队列中获取url url = queue.get() lock.acquire() current_url_count += 1 lock.release() print("【线程%d】队列中还有【%d】个URL,当前爬取的是第【%d】个URL:%s" % (threading_no, queue.qsize(), current_url_count, url)) # 判断url是否已爬取过,以防止重复落库 if url not in result_urls: page_message = get_page_message(url) page_url_list = page_message[0] page_title = page_message[1] page_text = page_message[2] if not page_message: continue # 将源码中的所有URL放到队列中 while page_url_list: url = page_url_list.pop() lock.acquire() if url not in result_urls: queue.put(url.strip()) lock.release() # 标题或正文包含关键字,才会入库 if key_word in page_title or key_word in page_text: lock.acquire() if not len(result_urls) >= crawl_num: sql = 'insert into crawl_page(url, title, text) values("%s", "%s", "%s")' % (url, page_title, page_text) mysql.insert(sql) result_urls.append(url) print("【线程%d】关键字【%s】,目前已爬取【%d】条数据,距离目标数据还差【%d】条,当前落库URL为【%s】" % (threading_no, key_word, len(result_urls), crawl_num-len(result_urls), url)) lock.release() else: # 已爬取的数据量达到要求,则结束爬虫 print("【线程%d】爬取总数【%d】达到要求,任务函数结束" % (threading_no, len(result_urls))) lock.release() return print("【线程%d】队列为空,任务函数结束" % threading_no) except: print("【线程%d】任务函数执行失败" % threading_no) traceback.print_exc()if __name__ == "__main__": # 爬取的种子页 home_page_url = "https://www.163.com" queue.put(home_page_url) mysql = MysqlTool("127.0.0.1", 3306, "test", "root", "admin") t_list = [] for i in range(50): t = Thread(target=get_html, args=(queue, lock, mysql, key_word, crawl_num, i)) time.sleep(0.05) t.start() t_list.append(t) for t in t_list: t.join()
执行结果:
十一、量化金融案例
量化金融是目前比较热门的一个多学科跨界融合领域,它综合运用金融知识、数学和统计知识、计算机信息技术来解决金融问题。
主要介绍Python在量化金融中的应用,包括股票数据的爬取、分析和可视化。
1、案例介绍
本案例涵盖了量化金融的以下几个方面:
- 大数据采集:通过爬虫获取特定行业(如汽车行业)股票的基本信息,并获取单只股票的历史行情数据。
- 大数据存储:根据自定义的时间间隔定时获取涨幅前60名股票的实时行情数据,并存储在数据库中。
- 大数据分析:计算股票的月涨跌幅,对股票进行相关性分析,并预测股票行情的未来走势。
2、获取汽车行业股票的基本信息
代码文件:获取汽车行业股票的股票代码和股票名称.py、
获取汽车行业股票的上市日期.py
要获取汽车行业股票的基本信息,包括股票代码、股票名称、上市日期。股票代码和股票名称从东方财富网爬取,上市日期则利用Tushare模块获取。
步骤1:首先确定爬取方案。在浏览器中打开东方财富网的汽车行业股票行情页面,网址为http://quote.eastmoney.com/center/boardlist.html#boards-BK04811。可以看到股票信息展示页面共8页,如下图所示。单击“下一页”按钮,发现网页并没有整体刷新,但是股票信息展示区的数据却刷新了,说明这部分数据是动态加载的,且页面使用的是局部刷新技术,因此,数据要么存储在JSON格式数据包中,要么存储在JS(JavaScript)文件中。
使用开发者工具进行全局搜索和数据定位,如搜索股票名称关键词“贵州轮胎”,会定位到一个JS文件,如下图所示。选择其他页码中的股票名称作为关键词进行搜索,则会定位到另一个JS文件。由此可知,每一页数据存储在不同的JS文件中。
针对这种情况,可以使用requests模块发起携带动态参数的请求来获取数据,也可以使用Selenium模块模拟用户操作来获取数据。后一种方法更方便,所以这里使用后一种方法。
步骤2:Selenium模块所起的作用主要是模拟用户操作浏览器对指定页面发起请求,并依次单击“下一页”按钮,从而获取所有页面的股票信息。
(1)向页面发起请求
实例化一个浏览器对象,对汽车行业股票行情页面发起请求。
代码如下:
1 from selenium import webdriver2 import time3 import pandas as pd4 browser = webdriver.Chrome(executable_path='chromedriver.exe') # 实例化浏览器对象5 url = 'http://quote.eastmoney.com/center/boardlist.html#boards-BK04811'6 browser.get(url) # 对指定页面发起请求
(2)获取单个页面的股票信息
先用开发者工具查看股票信息在网页源代码中的位置,可以发现表格数据存储在一个id属性值为table_wrapper-table的
标签中,股票代码在每一个标签下的第2个标签中,股票名称在每一个 标签下的第3个标签中,如下图所示。根据上述分析,写出相应的XPath表达式来定位这两个标签,并将标签中的数据提取出来,保存在一个字典中。
代码如下:
1 data_dt = {'股票代码':[], '股票名称':[]} # 用于存储爬取结果的字典2 def get_data(): # 获取每一页表格数据的自定义函数3 code_list = browser.find_elements_by_xpath('//*[@id='table_wrapper-table']/tbody/tr/td[2]') # 获取所有包含股票代码的标签,注意函数名中是“elements”而不是“element”4 name_list = browser.find_elements_by_xpath('//*[@id='table_wrapper-table']/tbody/tr/td[3]') # 获取所有包含股票名称的 标签,注意函数名中是“elements”而不是“element”5 for code in code_list:6 data_dt['股票代码'].append(code.text) # 将 标签中的股票代码依次提取出来并添加到字典中7 for name in name_list:8 data_dt['股票名称'].append(name.text) # 将 标签中的股票名称依次提取出来并添加到字典中(3)获取所有页面的股票信息
要获取所有页面的股票信息,需要知道总页数,以及如何定位“下一页”按钮。总页数最好不要直接使用在页面中看到的最后一页的页码,因为每天都可能有股票上市或退市,所以这个数字会变化。
用开发者工具查看页码和“下一页”按钮在网页源代码中的位置,可发现总页数位于class属性值为paginate_page的标签下的最后一个标签中,而“下一页”按钮可通过其class属性值next来定位,如下图所示。
根据上述分析编写代码,先获取总页数,再利用for语句构造循环,在每次循环中先调用前面编写的自定义函数获取当前页面的股票信息,再利用Selenium模块模拟用户操作单击“下一页”按钮,最终获取所有页面的股票信息。
代码如下:
1 pn_list = browser.find_elements_by_css_selector('.paginate_page>a') # 定位class属性值为paginate_page的标签下的所有标签2 pn = int(pn_list[-1].text) # 选取最后一个标签,提取其文本,再转换为整型数字,得到总页数3 for i in range(pn): # 根据获取的总页数设定循环次数4 get_data() # 调用自定义函数获取当前页面的股票信息5 a_btn = browser.find_element_by_css_selector('.next') # 通过class属性值定位“下一页”按钮6 a_btn.click() # 模拟单击“下一页”按钮7 time.sleep(5) # 因为是局部刷新,当前页面相当于未加载其他元素,用显式等待或隐式等待都比较麻烦,所以采用time模块进行简单的等待操作8 browser.quit() # 关闭浏览器窗口
(4)输出爬取结果
将字典转换为DataFrame,再存储为csv文件。
代码如下:
1 data_df = pd.DataFrame(data_dt)2 print(data_df)3 data_df.to_csv('汽车股票基本信息test.csv', index=False)
运行代码后,打开生成的csv文件,结果如下图所示。
步骤3:因为东方财富网的股票基本信息不包含上市日期,所以接着利用Tushare模块获取所有股票的基本信息,再将两部分信息依据股票代码进行合并,得到完整的汽车行业股票基本信息。
(1)获取所有股票的基本信息
使用Tushare模块中的get_stock_basics()函数可获取所有股票的基本信息。该函数返回的是一个DataFrame,其结构如下所示:
1 name industry area ... timeToMarket ...2 code3 688788 N科思 通信设备 深圳 ... 20201022 ...4 003013 N地铁 建筑工程 广东 ... 20201022 ...5 300849 锦盛新材 塑料 浙江 ... 20200710 ...6 300582 英飞特 半导体 浙江 ... 20161228 ...7 300279 和晶科技 元器件 江苏 ... 20111229 ...8 ......
可以看到,作为行标签的code列中的数据是股票代码,timeToMarket列中的数据是上市日期。只需保留timeToMarket列,然后将code列转换为普通的数据列。
代码如下:
1 import tushare as ts2 import pandas as pd3 data1 = pd.read_csv('汽车股票基本信息test.csv', converters={'股票代码': str}) # 读取从东方财富网爬取的股票基本信息,将“股票代码”列的数据类型转换为字符串,让以0开头的股票代码保持完整4 data2 = ts.get_stock_basics() # 获取所有股票的基本信息5 data2 = data2[['timeToMarket']].astype(str).reset_index() # 只保留timeToMarket列(即上市日期),并将其数据类型转换为字符串,然后重置索引,将行标签列转换为普通的数据列
(2)依据股票代码合并DataFrame
使用pandas模块中的merge()函数将data1和data2这两个DataFrame依据股票代码进行合并。代码如下:
1 data = pd.merge(data1, data2, how='left', left_on='股票代码', right_on='code') # 依据股票代码合并data1和data22 data = data.drop(['code'], axis=1) # 删除多余的列3 data = data.rename(columns={'timeToMarket': '上市日期'}) # 重命名列
第1行代码中,参数how设置为'left',表示在合并时完整保留data1的内容,并且依据股票代码如果能匹配到data2中的内容则合并进来,如果匹配不到则赋为空值;参数left_on和right_on分别用于指定data1和data2中股票代码所在的列。
(3)存储结果
将合并后的DataFrame存储为csv文件,并写入MySQL数据库。
代码如下:
1 from sqlalchemy import create_engine, types2 data.to_csv('汽车股票基本信息.csv', index=False)3 code_name_time_info.to_csv('汽车股票上市时间对照表.csv', index=False)4 con = create_engine('mysql+pymysql://root:123456@localhost:3306/test" /> 至此,获取汽车行业股票基本信息的任务就完成了。
3、获取单只股票的历史行情数据
代码文件:获取单只股票的历史行情数据.py
使用Tushare模块中的get_k_data()函数能获取单只股票的历史行情数据。下面使用该函数获取“拓普集团”(股票代码601689)的历史行情数据,并将数据按年份写入MySQL数据库。
步骤1:已将股票基本信息写入MySQL数据库,这里从数据库中查询“拓普集团”的股票基本信息,并从中提取上市年份。当前年份则利用datetime模块获取。
代码如下:
1 import tushare as ts2 from datetime import datetime3 from sqlalchemy import create_engine, types4 con = create_engine('mysql+pymysql://root:123456@localhost:3306/test?charset=utf8') # 创建数据库连接引擎5 connection = con.connect() # 创建connection对象来操作数据库6 data = connection.execute("SELECT * FROM car_stock WHERE 股票代码='601689';") # 查询“股票代码”字段值为'601689'的数据记录7 data_info = data.fetchall() # 提取查询到的数据,返回一个元组列表8 start_year = int(str(data_info[0][3])[:4]) # 从列表中取出第1个元组,再取出元组的第4个元素(即上市日期),然后通过切片提取前4个字符(即上市年份)9 end_year = datetime.now().year # 利用datetime模块获取当前年份
步骤2:使用get_k_data()函数获取每年的历史行情数据。
代码如下:
1 for year_num in range(start_year, end_year + 1):2 tuopu_info = ts.get_k_data('601689', start=f'{year_num}-01-01', end=f'{year_num}-12-31', autype='qfq') # 将参数autype指定为向前复权
步骤3:最后将获取的每年历史行情数据写入MySQL数据库。代码如下:
1 tuopu_info.to_sql(f'{year_num}_stock_data', con=con, index_label=['id'], if_exists='replace', dtype={'id': types.BigInteger(), 'date': types.DATETIME, 'open': types.VARCHAR(10), 'close': types.FLOAT(), 'high': types.FLOAT(), 'low': types.FLOAT(), 'volume': types.FLOAT(), 'code': types.INT()})
4、获取沪深A股涨幅前60名的信息
代码文件:获取沪深A股涨幅前60名的信息.py、将前60名的信息写入数据库.py
主要从东方财富网爬取沪深A股的实时行情信息,网址为http://quote.eastmoney.com/center/gridlist.html#hs_a_board。沪深A股的开盘时间为交易日的9:15—11:30和13:00—15:00。从13:00开始爬取,每爬取一次后间隔3分钟再爬取一次,如此循环往复,直到15:00为止。行情页面的行情信息是按照涨幅降序排列的,每页20条数据,因此,只需爬取前3页就能获取涨幅前60名的股票信息。
步骤1:导入需要用到的模块。
代码如下:
1 from selenium import webdriver2 import time3 import numpy as np4 import pandas as pd
步骤2:编写一个自定义函数top60()用于完成数据的爬取。用Selenium实例化一个浏览器对象,对行情页面发起请求,然后提取页面中表格的表头文本,在后续代码中作为数据的列标签。代码如下:
1 def top60():2 browser = webdriver.Chrome(executable_path='chromedriver.exe')3 url = 'http://quote.eastmoney.com/center/gridlist.html#hs_a_board'4 browser.get(url) # 对行情页面发起请求5 col_name = [] # 用于存储表头文本的列表6 col_tag = browser.find_elements_by_xpath('//*[@id="table_wrapper-table"]/thead/tr/th') # 定位表头中的所有单元格7 for i in col_tag:8 col_name.append(i.text) # 依次提取表头中每个单元格的文本并添加到列表中
步骤3:接着提取表格主体中所有单元格的文本,得到当前页面的表格数据,再切换页面,继续提取表格数据。这一系列操作需要重复3次。
代码如下:
1 data_ls = [] # 用于存储表格数据的列表2 for i in range(3): # 爬取前3页数据3 td_list = browser.find_elements_by_xpath('//*[@id="table_wrapper-table"]/tbody/tr/td') # 定位表格主体中的所有单元格4 for j in td_list:5 data_ls.append(j.text) # 依次提取表格主体中每个单元格的文本并添加到列表中6 a_btn = browser.find_element_by_css_selector('.next') # 定位“下一页”按钮7 a_btn.click() # 模拟单击“下一页”按钮8 time.sleep(2) # 等待2秒9 browser.quit() # 关闭浏览器窗口
步骤4:先利用NumPy模块将包含所有表格数据的列表转换为二维数组,再利用pandas模块将二维数组转换为DataFrame,并存储为Excel工作簿。
代码如下:
1 data_ar = np.array(data_ls).reshape(-1, len(col_name)) # 将包含所有表格数据的列表转换为一维数组,再使用reshape()函数将一维数组转换为二维数组,二维数组的列数设置为步骤2中提取的表头文本的个数,行数设置为-1,表示根据列数自动计算行数2 data_df = pd.DataFrame(data_ar, columns=col_name) # 将二维数组转换为DataFrame,使用步骤2中提取的表头文本作为列标签3 data_df.drop(['序号', '相关链接', '加自选'], axis=1, inplace=True) # 删除不需要的列4 data_df.rename(columns={'成交量(手)': '成交量', '市盈率(动态)': '市盈率'}, inplace=True) # 对部分列进行重命名5 now = time.strftime('%Y_%m_%d-%H_%M_%S', time.localtime(time.time())) # 生成当前日期和时间的字符串,用于命名Excel工作簿6 data_df.to_excel(f'涨幅排行top60-{now}.xlsx', index=False) # 将DataFrame存储为Excel工作簿
步骤5:编写一个自定义函数trade_time()用于判断股市当前是开市状态还是休市状态,判断的条件是当前星期是否在星期一到星期五之间,并且当前时间是否在13:00到15:00之间。
代码如下:
1 def trade_time():2 now = time.localtime(time.time()) # 获取当前日期和时间3 now_weekday = time.strftime('%w', now) # 获取当前星期4 now_time = time.strftime('%H:%M:%S', now) # 获取当前时间5 if ('1' <= now_weekday <= '5') and ('13:00:00' <= now_time <= '15:00:00'): # 判断股市状态的条件6 return True # 满足条件则返回True,表示当前处于开市状态7 else:8 return False # 否则返回False,表示当前处于休市状态
需要说明的是,要确定当天是否为交易日,除了要考虑当天是否为工作日(星期一到星期五),还要考虑当天是否为国家法定节假日。限于篇幅,上述代码只考虑了当天是否为工作日,感兴趣的读者可以利用搜索引擎查找判断当天是否为国家法定节假日的方法。
步骤6:用while语句构造一个条件循环,如果股市当前是开市状态,则爬取数据,如果是休市状态,则结束爬取。
代码如下:
1 while trade_time(): # 调用自定义函数trade_time()判断股市状态2 top60() # 如果是开市状态,则调用自定义函数top60()爬取数据3 time.sleep(180) # 等待3分钟后再重复操作4 print('股市已休市,结束爬取') # 如果是休市状态,则输出结束爬取的信息步骤7:读取Excel工作簿,将数据写入MySQL数据库。代码如下:1 from pathlib import Path2 import pandas as pd3 from sqlalchemy import create_engine, types4 file_list = Path(Path.cwd()).rglob('*.xlsx') # 列出当前文件夹下的所有Excel工作簿5 con = create_engine('mysql+pymysql://root:123456@localhost:3306/test?charset=utf8') # 创建数据库连接引擎6 for file in file_list:7 data = pd.read_excel(file, converters={'代码': str}) # 读取Excel工作簿8 data.to_sql(file.stem, con=con, index_label=['id'], if_exists='replace', dtype={'id': types.BigInteger(), '代码': types.VARCHAR(10), '名称': types.VARCHAR(10), '最新价': types.FLOAT, '涨跌幅': types.VARCHAR(10), '涨跌额': types.FLOAT, '成交量': types.VARCHAR(20), '成交额': types.VARCHAR(20), '振幅': types.VARCHAR(10), '最高': types.FLOAT, '最低': types.FLOAT, '今开': types.FLOAT, '昨收': types.FLOAT, '量比': types.VARCHAR(10), '换手率': types.VARCHAR(10), '市盈率': types.FLOAT, '市净率': types.FLOAT}) # 将数据写入数据库
主要介绍的爬虫程序都是在开发者自己的计算机上运行的,一旦计算机关机或出现故障,程序就停止运行了。如果要让程序24小时无间断运行,就要借助云服务器。云服务器可以24小时不关机,而且基本不会出现故障。将爬虫程序部署在云服务器上,就能实现数据的无间断自动定时爬取。
技巧:结合使用Selenium模块和pandas模块快速爬取表格数据
要爬取的行情数据是标准的表格数据,很适合使用pandas模块中的read_html()函数来提取。但是东方财富网的数据大多是动态加载的,read_html()函数无法直接处理。这里提供一种解决问题的思路:先用Selenium模块获取动态加载的网页源代码,再用read_html()函数从网页源代码中提取表格数据。
根据这一思路,将自定义函数top60()的代码修改如下:
1 def top60():2 browser = webdriver.Chrome(executable_path='chromedriver.exe')3 url = 'http://quote.eastmoney.com/center/gridlist.html#hs_a_board'4 browser.get(url) # 对行情页面发起请求5 data = pd.read_html(browser.page_source, converters={'代码': str})[0] # 利用page_source属性获取行情页面的网页源代码,传给read_html()函数,提取出第1页的表格数据,参数converters用于将“代码”列的数据类型转换为字符串,让以0开头的股票代码保持完整6 for i in range(2): # 前面已提取了第1页的表格数据,所以只需再循环2次7 time.sleep(2) # 等待2秒8 a_btn = browser.find_element_by_css_selector('.next') # 定位“下一页”按钮9 a_btn.click() # 模拟单击“下一页”按钮10 df = pd.read_html(browser.page_source, converters={'代码': str})[0] # 提取下一页的表格数据11 data = data.append(df, ignore_index=True) # 将提取的数据追加到data中12 browser.quit()13 data.drop(['序号', '相关链接', '加自选'], axis=1, inplace=True)14 data.rename(columns={'成交量(手)': '成交量', '市盈率(动态)': '市盈率'}, inplace=True)15 now = time.strftime('%Y_%m_%d-%H_%M_%S', time.localtime(time.time()))16 data.to_excel(f'涨幅排行top60-{now}.xlsx', index=False)
5、计算股票的月涨跌幅
代码文件:计算股票的月涨跌幅.py
一只股票一天的涨跌幅只能说明这只股票短期内的行情,要想分析一只股票的稳定性,还需要计算这只股票在较长一段时期内的涨跌幅。将获取“长城影视”(股票代码002071)在一年内的历史行情数据并计算月涨跌幅。
步骤1:利用Tushare模块获取“长城影视”从2019年6月1日到2020年5月31日的历史行情数据。
代码如下:
1 import tushare as ts2 import pandas as pd3 data_info = ts.get_hist_data(code='002071', start='2019-06-01', end='2020-05-31') # 获取一年的股票历史行情数据
步骤2:获取的数据是一个DataFrame,其行标签为交易日期,将行标签中交易日期的数据类型转换为datetime,方便进行数据的重新取样。
代码如下:
1 data_info.index = pd.to_datetime(data_info.index, format='%Y-%m-%d')
步骤3:对数据进行重新取样,获取每月第一天的数据和最后一天的数据。
代码如下:
1 month_first = data_info.resample('M').first() # 获取每月第一天的数据2 month_last = data_info.resample('M').last() # 获取每月最后一天的数据
步骤4:获取月初开盘价和月底收盘价,合并为一个新的DataFrame,然后计算出月涨跌幅。代码如下:
1 month_first = month_first['open'] # 获取月初开盘价2 month_last = month_last['close'] # 获取月底收盘价3 new_info = pd.DataFrame(list(zip(month_first, month_last)), columns=['月初开盘价', '月底收盘价'], index=month_last.index) # 合并月初开盘价和月底收盘价4 new_info['月涨跌幅'] = (new_info['月底收盘价'] - new_info['月初开盘价']) / new_info['月初开盘价'] # 计算月涨跌幅
步骤5:将最终结果存储为Excel工作簿。
代码如下:
1 new_info.to_excel('月涨跌幅.xlsx', index=False)
打开生成的Excel工作簿,其内容如下图所示。
6、股票相关性分析
代码文件:股票相关性分析.py
在股票投资中,有一种交易策略称为“配对交易”:找出历史股价走势相近的股票进行配对,当配对的股票价格差偏离历史均值时,在抛出股价高的股票的同时买进股价低的股票,等待配对的股票回归到长期的均衡关系,由此获利。
而衡量不同股票能否配对的关键就是股票之间的相关性是否足够大。下面就以“浦东金桥”(股票代码600639)和“新黄浦”(股票代码600638)这两只股票为例分析它们的相关性。
步骤1:获取“浦东金桥”和“新黄浦”从2019年6月1日到2020年5月31日的历史行情数据,并按日期排序(行标签为日期)。
代码如下:
1 import tushare as ts2 import pandas as pd3 import matplotlib.pyplot as plt4 plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']5 plt.rcParams['axes.unicode_minus'] = False6 pd_code = '600639' # “浦东金桥”的股票代码7 xhp_code = '600638' # “新黄浦”的股票代码8 pd_data = ts.get_hist_data(pd_code, start='2019-06-01', end='2020-05-31').sort_index() # 获取“浦东金桥”的历史行情数据并按日期排序9 xhp_data = ts.get_hist_data(xhp_code, start='2019-06-01', end='2020-05-31').sort_index() # 获取“新黄浦”的历史行情数据并按日期排序
步骤2:将不同股票每一天的收盘价合并为一个DataFrame。代码如下:
1 df = pd.concat([pd_data['close'], xhp_data['close']], axis=1, keys=['浦东金桥', '新黄埔']) # 提取收盘价并拼接成DataFrame2 print(df)
代码运行结果如下:
1 浦东金桥 新黄埔2 date3 2019-06-03 14.13 8.284 2019-06-04 14.16 8.375 2019-06-05 14.05 8.376 ... ... ...7 2020-05-27 14.69 5.488 2020-05-28 14.87 5.419 2020-05-29 14.56 5.45
步骤3:使用corr()函数计算皮尔逊相关系数,判断收盘价的相关性。
代码如下:
1 corr = df.corr()2 print(corr)
代码运行结果如下:
1 浦东金桥 新黄埔2 浦东金桥 1.000000 0.6367783 新黄埔 0.636778 1.000000
可以看到,近一年两只股票收盘价的皮尔逊相关系数约为0.64,说明两只股票的相关性较弱,不满足做匹配交易的条件。
步骤4:绘制收盘价走势图,将数据可视化。
代码如下:
1 df.plot()2 plt.show()
代码运行结果如下图所示。可以看到,在2019年8月到2020年1月,这两只股票的收盘价走势比较相似,这时进行配对交易,盈利的可能性更大。
7、股票价格预测
代码文件:股票价格预测.py、stocker.py、预测模型结果图.tif
将利用stocker模块根据一只股票的历史行情数据预测这只股票未来的行情。需要说明的是,证券市场变幻莫测,预测结果只能大致反映股价走势,并不能作为证券交易的依据。
下面以“酒鬼酒”(股票代码000799)为例讲解具体操作。
步骤1:stocker模块是用于预测股票行情的Python第三方开源模块,项目网址为https://github.com/WillKoehrsen/Data-Analysis/tree/master/stocker。该模块的安装不是通过pip命令,而是从项目网址下载stocker.py文件后放到项目文件夹中,或者在项目文件夹中新建stocker.py文件,再在网页中打开stocker.py,复制其中的代码,将其粘贴到新建的stocker.py文件中。
准备好stocker.py文件后,还要通过pip命令安装stocker模块需要调用的其他Python第三方模块,包括quandl 3.3.0、Matplotlib 2.1.1、NumPy 1.14.0、fbprophet 0.2.1、pystan 2.17.0.0、pandas 0.22.0、pytrends 4.3.0。
步骤2:stocker模块默认使用quandl模块获取股票数据。因为quandl模块获取数据不太方便,所以这里通过修改stocker模块的代码,使用pandas_datareader模块获取股票数据。
先用pip命令安装好pandas_datareader模块,然后在PyCharm中打开stocker.py文件,在开头添加代码,导入pandas_datareader模块中的data子模块,具体如下:
1 from pandas_datareader import data
接下来修改Stocker类的构造函数__init__()的代码,更改Stocker类接收的参数。通过搜索关键词“__init__”定位到如下所示的代码:
1 class Stocker():2 # Initialization requires a ticker symbol3 def __init__(self, ticker, exchange='WIKI'):
删除上述第3行代码中的参数exchange,保留参数ticker,添加两个新参数start_date和end_date,分别代表股票数据的开始日期和结束日期。
修改后的代码如下:
1 class Stocker():2 # Initialization requires a ticker symbol3 def __init__(self, ticker, start_date, end_date):
接下来使用pandas_datareader模块中的data子模块替换原来的quandl模块获取股票数据。通过搜索关键词“quandl”定位到如下所示的代码:
1 try:2 stock = quandl.get('%s/%s' % (exchange, ticker))3 except Exception as e:4 print('Error Retrieving Data.')
将上述第2行代码注释掉,在其下方添加一行代码,获取雅虎财经的股票行情数据。
修改后的代码如下:
1 try:2 # stock = quandl.get('%s/%s' % (exchange, ticker))3 stock = data.get_data_yahoo(ticker, start_date, end_date)4 except Exception as e:5 print('Error Retrieving Data.')
这样就完成了对stocker模块的代码的修改。
步骤3:新建一个Python文件,开始编写预测股票价格的代码。先实例化一个Stocker对象,再使用该对象获取“酒鬼酒”的历史行情数据,并将数据绘制成图表。
代码如下:
1 from stocker import Stocker2 alcoholic = Stocker('000799.SZ', start_date='2000-01-01', end_date='2020-05-31') # 获取历史行情数据3 alcoholic.plot_stock() # Stocker内部封装的画图方法
需要注意第2行代码中股票代码的书写格式“股票代码.交易所缩写”。上海证券交易所的缩写为“SS”,深圳证券交易所的缩写为“SZ”。代码运行结果如下图所示,因为数据是从雅虎财经获取的,所以货币单位为美元。
步骤4:使用create_prophet_model()函数生成一个预测模型结果图。
代码如下:
1 model, model_data = alcoholic.create_prophet_model(days=90)
代码运行结果如下图所示:
图中黑色的线是观察线(Observations),是根据股票的真实行情数据绘制的;绿色的线是根据模型预测的行情数据绘制的;浅绿色的图形代表置信区间,表示预测结果会在这个区间内波动。
通过分析预测模型结果图可知,模型预测股价在6月左右会下降一点,在7—9月会缓慢上升。但是在5月底,真实股价的涨势很快,超出了模型预测的置信区间,说明当时可能发生了影响股价的特殊事件。通过搜索新闻可知,当时“酒鬼酒”公司调整了酒价,导致股价上涨。这也说明了影响股市的因素很多,模型的预测结果不能作为交易的依据。
步骤5:预测30天后的股价。
代码如下:
1 alcoholic.predict_future(days=30)
代码运行结果如下图所示。
可以看到,由于酒价调整,预测结果并不准确。