这一节实践其实是在上一节之前进行的,背景原因是因为tushare.pro的积分不够高,当时还没有接触到使用akshare等其他接口,因此对于全股票列表用的是去网页上爬的方式获得的,也就借此机会,再复习了一遍爬虫的相关知识。
这一节主要是关于爬虫的一些实践的记录,也可以获取到可以交给backtrader进行回测的一系列的数据,但相对而言麻烦了一些,没有兴趣可以跳过,完全不影响backtrader的实践。
python爬虫基础
01_python爬虫基础内容总结
当前比较有效的学习方式就是有问题问AI,我们在几个AI助手里都输入:“请帮我快速总结 python爬虫基础,以列表的形式显示” 这样的提问,然后获取一段自己觉得接受程度最高的内容:
请求模块
requests
:发送HTTP请求获取网页内容。http.client
:Python标准库中用于HTTP请求的模块。解析工具
BeautifulSoup
:解析HTML或XML文档。lxml
:更高效的HTML/XML解析库。re
(正则表达式):文本模式匹配和提取数据。处理JavaScript
Selenium
:控制浏览器执行JavaScript渲染后的页面抓取。Pyppeteer
或Puppeteer
:无头浏览器操作,异步JavaScript渲染页面。数据存储
- 文件写入:使用
open()
保存到本地文件。- 数据库:如SQLite, MySQL等数据库存储。
遵守规则
robots.txt
:检查网站是否允许爬虫访问特定URL。- 尊重
User-Agent
:设置合适的请求头。错误处理
- 异常捕捉:处理网络异常、解析错误等。
- 重试机制:网络不稳定时自动重试。
效率提升
- 多线程/多进程:并发请求提高爬取速度。
asyncio
:异步IO,适用于高并发场景。合法性与道德
- 遵守法律法规,避免侵犯隐私或版权。
- 控制爬取频率,减轻服务器负担。
调试与测试
- 使用开发者工具检查网络请求。
- 单元测试确保代码质量。
框架支持
Scrapy
:强大的爬虫框架,适合大规模项目。
接着,就把requests库的典型应用复习一遍。
02_爬取翻译的实践
001_单个单词的翻译
这是一个典型的python爬虫的简便应用,它通过requests库模拟了我们在浏览器中的操作,比如说查询dog的中文翻译,我们可以通过浏览器自带的“Inspect / 审查”功能观察到它的信息包括headers,response,通过preview查看就明白网页上显示的就是data里前四条。
使用requests的代码如下
# 0 导包
import requests
import json# 1. url与data
headers = {'User-Agent':'Mozilla/5.0 xxxxxxxxxxxx'
}post_url = 'https://fanyi.baidu.com/sug'data = { 'kw':'dog', }# 2. 发起请求
response = requests.post(url = post_url, data = data, headers = headers)dic_obj = response.json()
dic_obj---------------------
{'errno': 0,'data': [{'k': 'dog', 'v': 'n. 狗; 蹩脚货; 丑女人; 卑鄙小人 v. 困扰; 跟踪'},{'k': 'DOG', 'v': 'abbr. Data Output Gate 数据输出门'},{'k': 'doge', 'v': 'n. 共和国总督'},{'k': 'dogm', 'v': 'abbr. dogmatic 教条的; 独断的; dogmatism 教条主义; dogmatist'},{'k': 'Dogo', 'v': '[地名] [马里、尼日尔、乍得] 多戈; [地名] [韩国] 道高'}],'logid': 2057633931}
002_多个单词的翻译与保存
假设我们有一个英文单词的列表,那么就需要循环的每次取一个单词出来,通过requests把中文翻译得到,并放到中文的列表里。
all_data = ['dog', 'wolf', 'rabbit', 'sheep', 'sleep']import requests
def get_translation(list1):url = 'https://fanyi.baidu.com/sug'headers = {'User-Agent':'Mozilla/5.0 ......'
}ret_data = []for x in list1:word1 = xdata1 = {'kw': word1}response = requests.post(url = url, data = data1, headers = headers)dic_obj = response.json()ret_data.append(dic_obj['data'][0]['v'])return ret_dataret = get_translation(all_data)
ret------------------------
['n. 狗; 蹩脚货; 丑女人; 卑鄙小人 v. 困扰; 跟踪','n. 狼; 贪婪的人; 有害的幼虫 vt. 狼吞虎咽','n. 兔子,野兔; 兔子皮毛; 兔子肉; 〈俚〉新手,弱手 vi. 猎兔(通常作 go rabbit','n. 羊,绵羊; 胆小鬼; 易受人摆布的人; 缺乏主见或轻易盲从的人','vi.& link-v. 睡,睡觉 vi. 睡,睡觉; 睡眠状态 vt. 为…提供床位; 提供住宿;']
然后可以用Pandas的DataFrame把它们保存下来
En CH
No.
1 dog n. 狗; 蹩脚货; 丑女人; 卑鄙小人 v. 困扰; 跟踪
2 wolf n. 狼; 贪婪的人; 有害的幼虫 vt. 狼吞虎咽
3 rabbit n. 兔子,野兔; 兔子皮毛; 兔子肉; 〈俚〉新手,弱手 vi. 猎兔(通常作 go ra...
4 sheep n. 羊,绵羊; 胆小鬼; 易受人摆布的人; 缺乏主见或轻易盲从的人
5 sleep vi.& link-v. 睡,睡觉 vi. 睡,睡觉; 睡眠状态 vt. 为…提供床位; 提...
003_get与post
还是交给AI来完成:
在Web开发中,
POST
和GET
是两种常用的 HTTP 请求方法,它们有以下主要区别:
用途:
GET
:通常用于从服务器检索信息。数据会附加在URL中作为查询参数。POST
:用于向服务器发送数据,通常用于表单提交。数据不会显示在URL中。安全性:
GET
:不太安全,因为数据暴露在URL中,容易被缓存或记录在浏览历史中。POST
:相对更安全,数据通过请求体发送,不会出现在URL中。数据大小限制:
GET
:数据长度受限于URL长度限制(不同浏览器有不同的限制)。POST
:理论上没有数据大小限制。可缓存性:
GET
:请求可以被浏览器缓存。POST
:通常不可缓存。幂等性:
GET
:多次执行同一个GET请求效果等同于一次执行。POST
:多次执行可能有副作用,例如多次提交表单可能导致多次记录创建。
# get 示例
import requestsurl = 'https://api.example.com/data'
params = {'key1': 'value1', 'key2': 'value2'}response = requests.get(url, params=params)print(response.url) # 输出: https://api.example.com/data?key1=value1&key2=value2
print(response.text)# post 示例
import requestsurl = 'https://api.example.com/data'
data = {'key1': 'value1', 'key2': 'value2'}response = requests.post(url, data=data)print(response.status_code)
print(response.text)
从代码上,简单的区别是get要用到params,而post用到data。
爬取股票数据
原本我们进行backtrader回测需要好几个不同的数据,例如全股票列表,ETF列表,单支股票的历史行情......,前面也说了在有akshare或其他接口的情况下,不需要花费太多的时间在每个需要的数据上进行爬取,这里简单的实践2个页面。
01_全股票列表
全股票列表,可以从各种不同的方向来获取,比如说选股器,又比如说行情中心的沪深京A股
第一种方式从选股器进,选择全部A股,每页显示50个,共108页,选择根据股票代码排序。
第二种方式从行情中心进,选择沪深京A股,每页显示20个,共283页。
以第一种方式为例,首先在页面上右键--审查,然后在右侧“网络”--Fetch/XHR中,找到网页上表格对应的文件,在预览里看到数据和网页上一一对应。
然后,回到标头(header)页,复制URL。这里可以看到请求方法是GET,也可以看到每页显示50个(ps=50),当前第1页(p=1)。
直接在浏览器的地址栏里粘贴这段url并回车,就会得到下面这样的页面:
{"version": null,"result": {"nextpage": true,"currentpage": 1,"data": [{"SECUCODE": "000001.SZ","SECURITY_CODE": "000001","SECURITY_NAME_ABBR": "平安银行","NEW_PRICE": 9.65,"CHANGE_RATE": -2.53,"VOLUME_RATIO": 1.17,"HIGH_PRICE": 9.88,"LOW_PRICE": 9.63,"PRE_CLOSE_PRICE": 9.9,"VOLUME": 1108727,"DEAL_AMOUNT": 1078602467.68,"TURNOVERRATE": 0.57,"MARKET": "深交所主板","MAX_TRADE_DATE": "2024-09-11"},{"SECUCODE": "000002.SZ","SECURITY_CODE": "000002","SECURITY_NAME_ABBR": "万科A","NEW_PRICE": 6.31,"CHANGE_RATE": 0.16,"VOLUME_RATIO": 0.55,"HIGH_PRICE": 6.38,"LOW_PRICE": 6.24,"PRE_CLOSE_PRICE": 6.3,"VOLUME": 604082,"DEAL_AMOUNT": 381296490.68,"TURNOVERRATE": 0.62,"MARKET": "深交所主板","MAX_TRADE_DATE": "2024-09-11"},{"SECUCODE": "000004.SZ","SECURITY_CODE": "000004","SECURITY_NAME_ABBR": "国华网安",
由上,这个URL就能得到50个股票的列表:
import requests#1 指定URL
url = "https://data.eastmoney.com/dataapi/xuangu/list?st=SECURITY_CODE&sr=1&ps=50&p=1&sty=SECUCODE%2CSECURITY_CODE%2CSECURITY_NAME_ABBR%2CNEW_PRICE%2CCHANGE_RATE%2CVOLUME_RATIO%2CHIGH_PRICE%2CLOW_PRICE%2CPRE_CLOSE_PRICE%2CVOLUME%2CDEAL_AMOUNT%2CTURNOVERRATE%2CMARKET&filter=(MARKET+in+(%22%E4%B8%8A%E4%BA%A4%E6%89%80%E4%B8%BB%E6%9D%BF%22%2C%22%E6%B7%B1%E4%BA%A4%E6%89%80%E4%B8%BB%E6%9D%BF%22%2C%22%E6%B7%B1%E4%BA%A4%E6%89%80%E5%88%9B%E4%B8%9A%E6%9D%BF%22%2C%22%E4%B8%8A%E4%BA%A4%E6%89%80%E7%A7%91%E5%88%9B%E6%9D%BF%22%2C%22%E4%B8%8A%E4%BA%A4%E6%89%80%E9%A3%8E%E9%99%A9%E8%AD%A6%E7%A4%BA%E6%9D%BF%22%2C%22%E6%B7%B1%E4%BA%A4%E6%89%80%E9%A3%8E%E9%99%A9%E8%AD%A6%E7%A4%BA%E6%9D%BF%22%2C%22%E5%8C%97%E4%BA%AC%E8%AF%81%E5%88%B8%E4%BA%A4%E6%98%93%E6%89%80%22))&source=SELECT_SECURITIES&client=WEB"#2 发起请求 + #3 获取响应数据
response = requests.get(url=url)
dic1 = response.json()import pandas as pd
import jsond3 = dic1['result']['data']df = pd.DataFrame(d3)
df.iloc[:,:5]--------------------------------SECUCODE SECURITY_CODE SECURITY_NAME_ABBR NEW_PRICE CHANGE_RATE
0 000001.SZ 000001 平安银行 9.65 -2.53
1 000002.SZ 000002 万科A 6.31 0.16
2 000004.SZ 000004 国华网安 11.85 -2.63
3 000006.SZ 000006 深振业A 4.11 -1.44
4 000007.SZ 000007 全新好 4.64 -2.52
5 000008.SZ 000008 神州高铁 1.99 -5.24
6 000009.SZ 000009 中国宝安 7.64 0.00
循环108次,每次把p=x的值加1,这样就能得到全股票列表了。
02_添加拼音首字母缩写
这个动作在第2节里已经完整的实践过,这里就不重复了。
- 量化交易backtrader实践(一)_数据获取篇(2)_tushare与akshare-CSDN博客
03_个股历史行情数据
001_获取历史行情数据的URL
这一段在之前的实践中并不存在,即使之前用tushare.pro不够积分获取全股票列表,但仍然可以请求日线也就是历史行情数据的,所以这一项也从网页上爬取并没有太多的意义。
不过既然咱们这一节本身就属于一个番外的内容,索性就在爬虫的道路上多走一步。而且,这里跟前面我们讲的还都不太一样。
这里的历史行情数据属于动态加载,但它不是Fetch/XHR里(即不是json数据),而是在JS文件中,这种数据要找到它都会比较的困难,我每次都是一个一个找。
在通过预览找到了对应的JS文件之后,再通过标头(header)得到URL,然后在浏览器的地址栏粘贴这个URL,就得到如下图所示,从2024/3/20到2024/9/11的Klines了。
这里显示的时间段只有120天(半年),这点数据放到backtrader里回测似乎有点少,想要增加数据的量,可以直接从负载页看参数:
那么在地址栏中的这部分lmt=120改为240就可以得到差不多一年的数据了,需要更多的数据那么可以改为400,600等。
于是我们可以写如下代码:
import requests
url1 = "http://11.push2his.eastmoney.com/api/qt/stock/kline/get?
cb=jQuery35108492299702581476_1726063980126&secid=1.601600
&ut=fa5fd1943c7b386f172d6893dbfba10b&fields1=f1%2Cf2%2Cf3%2Cf4%2Cf5%2Cf6
&fields2=f51%2Cf52%2Cf53%2Cf54%2Cf55%2Cf56%2Cf57%2Cf58%2Cf59%2Cf60%2Cf61
&klt=101&fqt=1&end=20500101&lmt=400&_=1726063980130"#2 发起请求 + #3 获取响应数据
response = requests.get(url=url1)
page_text = response.text
page_text------------------------
'jQuery35108492299702581476_1726063980126({"rc":0,"rt":17,"svr":177617930,"lt":1,"full":0,"dlmkts":"",
"data":{"code":"601600","market":1,"name":"中国铝业","decimal":2,"dktotal":4081,"preKPrice":4.89,
"klines":["2023-01-19,4.87,4.91,4.91,4.85,704244,352357028.00,1.23,0.41,0.02,0.54",
"2023-01-20,4.92,5.08,5.10,4.89,1765222,906405629.00,4.28,3.46,0.17,1.35",
......
"2024-09-10,6.35,6.30,6.38,6.20,975358,613191159.00,2.84,-0.63,-0.04,0.74",
"2024-09-11,6.28,6.25,6.30,6.13,1000724,620802872.00,2.70,-0.79,-0.05,0.76"]}});'
到这里,我们遇到一个问题,即文本是以'jQuery开头的,并不是一个json的数据,我们需要怎么做?
002_从JS代码中获取数据
根据多年来的习惯,我还是会先思考自己能采用什么方式,这些方式里有没有简洁高效的,然后才会去问AI怎么解决。
自己的思考是最开始括号"("和最后")"里的应该是json数据,那么用.find()找到这两个索引,用切片的方式就可以了
import jsonistart=page_text.find("(")
istop =page_text.rfind(")")page_data = page_text[istart+1:istop]json_data = json.loads(page_data)
然后还是问了AI: requests请求得到的数据格式如下,怎么直接转换成json的数据? 得到的代码如下
import json# 假设这是你的原始响应文本
response_text = page_text# 移除包裹在JSON外面的JavaScript函数调用
trimmed_response = response_text.strip().split('(', 1)[1].rsplit(')', 1)[0]# 将JSON字符串转换为Python字典
json_data = json.loads(trimmed_response)
json_data--------------------------
{'rc': 0,'rt': 17,'svr': 177617930,'lt': 1,'full': 0,'dlmkts': '','data': {'code': '601600','market': 1,'name': '中国铝业','decimal': 2,'dktotal': 4081,'preKPrice': 4.89,'klines': ['2023-01-19,4.87,4.91,4.91,4.85,704244,352357028.00,1.23,0.41,0.02,0.54','2023-01-20,4.92,5.08,5.10,4.89,1765222,906405629.00,4.28,3.46,0.17,1.35','2023-01-30,5.17,5.14,5.20,5.06,1677315,879133051.00,2.76,1.18,0.06,1.28',......'2024-09-10,6.35,6.30,6.38,6.20,975358,613191159.00,2.84,-0.63,-0.04,0.74','2024-09-11,6.28,6.25,6.30,6.13,1000724,620802872.00,2.70,-0.79,-0.05,0.76']}}
看了AI给出的代码,也就是巧妙用split两次得到中间的部分,又一次增长了知识。
003_提取数据到DataFrame
得到json数据后之后,需要把'data'里面的"klines"的值进一步提取,然后里面的数据用逗号进行分割,再转成DataFrame类型就可以了。
klines1 = json_data['data']['klines'] # 提取kline的数据listlisttmp = []
for x in klines1:list1 = x.split(',')listtmp.append(list1)import pandas as pd
columns1 = ['date','open','high','low','close','volume','amount','x','x2','x3','x4']
df_klines = pd.DataFrame(listtmp, columns=columns1)
df_klines.iloc[:,:7]-------------------------------date open high low close volume amount
0 2023-01-19 4.87 4.91 4.91 4.85 704244 352357028.00
1 2023-01-20 4.92 5.08 5.10 4.89 1765222 906405629.00
2 2023-01-30 5.17 5.14 5.20 5.06 1677315 879133051.00
3 2023-01-31 5.12 5.17 5.23 5.07 1141659 603112241.00
4 2023-02-01 5.22 5.42 5.52 5.20 2328566 1284011402.00
... ... ... ... ... ... ... ...
395 2024-09-05 6.62 6.46 6.68 6.38 1335513 866917546.00
396 2024-09-06 6.50 6.46 6.52 6.43 686603 444664992.00
397 2024-09-09 6.38 6.34 6.41 6.24 1130286 712609591.00
398 2024-09-10 6.35 6.30 6.38 6.20 975358 613191159.00
399 2024-09-11 6.28 6.25 6.30 6.13 1000724 620802872.00
400 rows × 7 columns
这个DataFrame的数据再进行交给backtrader前的格式处理,就可以把数据送到backtrader里进行回测了。
本节小结
这一节是由于前面某个时间段里没有找到好用的金融数据接口包,又想着把python爬虫学到的一点皮毛来实践一下而出现的。通过这一部分的实践,可以让我们不依赖金融数据接口包的情况下,获取需要的数据并用于backtrader回测。
同时经过与金融数据接口包获取数据的对比,发现了自己对股票市场很多东西理解深度还很低,有很多需要改进的地方,虽然对于整个backtrader实践流程没有太多的帮助,但对交易系统的理解和感受又有了一定的进步。