前言
在之前《用Python爬取双色球开奖信息(升级版)》中已经介绍了简单的urllib+re正则的方式来提取每天的双色球数据,当然这是有用的,虽然数据量少,但是可以用来做一些比如“买了股票自动比对中奖情况然后推送”这一类程序或网页。
但这种爬取方式仍然存在问题:容易被网站的反爬虫或者反作弊发现。也就是说,你爬取这些接口,那边的服务器系统会有日志的,并且有自动处理程序,甚至会有机器学习的程序。尽管这种数据没有什么敏感性,根本不会来封你的IP,不过也要养成良好的爬虫习惯,至少——在爬取的时候加个header,不要被一句简单的awk命令就给筛选出来安排得明明白白了(我在实习的时候经常一句awk就筛出那些刷金币刷花接口的小同学,尽管大佬们提供了svm机器学习模型来自动处理)。
注意到有的网站提供了大量的历史开奖数据,比如500彩票网双色球历史数据、中彩网双色球历史数据,我们可以都爬下来,然后自行对比使用,一个可能的用途是用机器学习去预测走势(虽然国内的双色球……em……每天停售后两个小时才开奖……可以做很多事……而且如果完全公正了,从概率上来说机器学习学出的中奖概率是一样的……),用来玩玩吧。
运行环境
解释器:python3.5.2
IDE:Pycharm 2016.3.2
浏览器:Chrome
爬取500双色球的历史数据
500彩票网的网页比较规矩,没有什么花里胡哨的东西。
1、进入网页,查看接口
用Chrome浏览器进入500双色球历史数据网页,然后右键
→检查
,翻到Network
→XHR
的一栏。
网页上随便选择期数,点击查看
,就能看到网页异步获取数据的接口了:
点进去看一看,非常简单的get请求:
翻看一下Response,竟然直接返回的HTML而不是JSON……
2、分析数据
这就比较烦了,需要解析html了,首先分析一下彩票数据html的数据结构:
<tbody id="tdata">
<tr class="t_tr1">
<!--<td>2</td>-->
<td>18092</td>
<td class="t_cfont2">06</td>
<td class="t_cfont2">10</td>
<td class="t_cfont2">16</td>
<td class="t_cfont2">19</td>
<td class="t_cfont2">24</td>
<td class="t_cfont2">33</td>
<td class="t_cfont4">16</td>
<td class="t_cfont4"> </td>
<td>921,043,817</td>
<td>1</td>
<td>10,000,000</td>
<td>95</td>
<td>269,198</td>
<td>318,819,890</td>
<td>2018-08-09</td>
</tr>
<tr class="t_tr1">
<!--<td>2</td>-->
<td>18091</td>
<td class="t_cfont2">06</td>
<td class="t_cfont2">11</td>
<td class="t_cfont2">13</td>
<td class="t_cfont2">17</td>
<td class="t_cfont2">25</td>
<td class="t_cfont2">32</td>
<td class="t_cfont4">07</td>
<td class="t_cfont4"> </td>
<td>854,322,188</td>
<td>4</td>
<td>8,999,907</td>
<td>81</td>
<td>246,907</td>
<td>315,853,082</td>
<td>2018-08-07</td>
</tr>
</tbody>
非常完整标准的表格结构,这里要注意几个细节:
(1)中间有一栏是 ,也就是空格,这一栏的体现是一个名为快乐星期天的属性。快乐星期天是指开出两个蓝球,第一个正常开,该中啥中啥;第二个蓝号中了,并且前面红号任意中5个,就能得到固定的3000元奖金。
这里由于只是一个活动,不清楚以后是否会用,而且快乐星期天数据格式有:空格( ,unicode显示为\xa0);0;1个数字;3个逗号分隔的数字,因此可以考虑把这个属性变为用“-”连接的字符串,如果以后有需要可以自行处理,如果不需要可以直接略过整个属性。
(2)数字采用了银行的那种逗号分隔的格式,这是方便浏览的格式;同时我们想做的也是用逗号分隔,这会产生歧义,并且我们需要的是存储使用而不是观看,所以需要在采集的时候把逗号去掉。
(3)多测几次数据,会发现500没有对数据进行分页,这对爬取来说非常有利。
3、开始爬取
requests
HTMLParser
def getHtml(url, method='get', headers={}, params={}):
if method == 'get':
html = requests.get(url, headers=headers, params=params)
html.encoding = 'utf-8' #html.apparent_encoding
else:
html = requests.post(url, headers=headers, data=params)
html.encoding = 'utf-8'
return html.text
header和params都是这样的字典:
headers:
{
"Host": "datachart.500.com",
"Connection": "keep-alive",
"Accept": "*/*",
"X-Requested-With": "XMLHttpRequest",
"User-Agent": "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62."
"0.3202.94 Safari/537.36",
"Referer": "http://datachart.500.com/ssq/history/history.shtml",
"Accept-Encoding": "gzip, deflate",
"Accept-Language":"zh-CN,zh;q=0.9",
"Cookie":"ck_RegFromUrl=http%3A//www.500.com/; sdc_session=1533916356798; _jzqy=1.1528606472.1533916357.1."
"jzqsr=baidu.-; _jzqckmp=1; seo_key=baidu%7C%7Chttps://www.baidu.com/link?url=MQcB3-er5rK249eCQNw"
"G8A4N-hontstLi8HIy9E7H_q&wd=&eqid=fec80b7400019715000000035b6db4d7; bdshare_firstime=15339164028"
"59; WT_FPC=id=undefined:lv=1533916553308:ss=1533916402791; _qzja=1.190448226.1533829920533.15338"
"29920534.1533916402865.1533916409059.1533916553371.0.0.0.5.2; _qzjc=1; _jzqa=1.20620370185900751"
"00.1533829921.1533829921.1533916357.2; _jzqc=1; Hm_lvt_4f816d475bb0b9ed640ae412d6b42cab=15338299"
"21,1533916357; Hm_lpvt_4f816d475bb0b9ed640ae412d6b42cab=1533916554; __utma=63332592.612539621.15"
"33916358.1533916358.1533916358.1; __utmc=63332592; __utmz=63332592.1533916358.1.1.utmcsr=baidu|u"
"tmccn=(organic)|utmcmd=organic; CLICKSTRN_ID=123.98.36.94-1533829945.911636::415C872F9F26292A610C"
"843E6BD7FEB2; motion_id=1533920615560_0.37147948464863223",
}
params:
{
'start':'1',
'end':'18092'
}
start和end就是那个get请求的URL携带的参数啦,就是彩票期数。Cookie可以直接加在header里面发送,也可以单独地作为参数:
requests.get(url, headers=headers, cookies=cookies, params=params)
前面提到,爬下来是一个网页,需要解析,我们构建一个HTMLParser的处理类:
class Parser500ssq(HTMLParser):
flag_tbody = False # 定义一些变量
flag_tr = False
flag_td = False
linedata = []
result = []
def handle_starttag(self, tag, attrs): # 匹配开始标签
if (str(tag).startswith("tbody")): # 比如匹配tbody
for k,v in attrs: # 遍历tbody标签中的属性
if k == 'id' and v == 'tdata': # 如果匹配到指定属性
self.flag_tbody = True # 设置一个flag,表明匹配到了开始工作啦
return
elif (self.flag_tbody == True):
if (str(tag).startswith("tr")):
self.flag_tr = True
if (str(tag).startswith("td")):
self.flag_td = True
def handle_endtag(self, tag): # 匹配结束标签
if (self.flag_tbody == True): # 如果在tbody标签里面
if (str(tag).startswith("tr")): # 如果是</tr>,说明行结束了,提交一波数据
self.result.append(self.linedata)
self.linedata = []
self.flag_tr = False
elif (str(tag).startswith("td")): # 如果是</td>,说明一个属性结束了,关掉flag
self.flag_td = False
elif (str(tag).startswith("tbody")):
self.flag_tbody = False
def handle_data(self, data): # 处理<xx>data</xx>里面的数据
if (self.flag_td == True): # 如果是在处理<td>内的数据
if '\xa0' in data:
self.linedata.append("-".join(data.replace(u'\xa0', u'').split(","))) # 空格特殊处理
else:
self.linedata.append("".join(data.split(","))) # 数字去掉逗号
'''
def handle_startendtag(self, tag, attrs): # 匹配开始和结束标签,用不到
print('<%s/>' % tag)
def handle_comment(self, data): # 匹配<!-->comment<--!>这样的注释,用不到
print('<!--', data, '-->')
def handle_entityref(self, name): # 匹配特殊字符,比如&nbps,用不到
print('&%s;' % name)
def handle_charref(self, name): # 匹配特殊字符串,比如&#,用不到
print('&#%s;' % name)
'''
思路很简单,匹配到了开始符就设置flag,然后就开始收集数据,匹配到了结束符就存一波数据,这样数据就会被以列表的结构存在result里面。那么我们简单定义一个处理函数,就能够把它打入文件保存了。用feed
给这个parser类喂数据,用with...as
的结构来写文件:
def parse500ssq(html,datapath):
print("start grabbing...")
parser = Parser500ssq()
parser.feed(html)
print(parser.result)
with open(BASEPATH + datapath, 'w') as f:
for line in parser.result:
print(line)
f.write(",".join(line))
f.write("\n")
print("finished.")
得到的结果是这样的:
['18092', '06', '10', '16', '19', '24', '33', '16', '', '921043817', '1', '10000000', '95', '269198', '318819890', '2018-08-09']
['11025', '08', '25', '26', '31', '32', '33', '09', '0', '310916704', '4', '8040679', '80', '228050', '335345846', '2011-03-06']
['06050', '02', '06', '12', '15', '25', '31', '07', '09-14-05', '138448657', '1', '5000000', '39', '148511', '94915860', '2006-05-02']
['03001', '10', '11', '12', '13', '26', '28', '11', '10', '2097070', '0', '0', '1', '898744', '10307806', '2003-02-23']
爬取中彩网双色球的历史数据
1、进入网页,查看接口
进入中彩网双色球往期查询页面,相同的方法,随便选择期数,点击查询
,观察XHR。
然而XHR没有任何变化……
检查查询按钮的HTML,是一个调用js函数search()的input元素:
网页上右键
,查看网页源代码
,ctrl+f
搜索search(),竟然没搜到……
仔细观察发现是加载了一个iframe:
<iframe src="http://kaijiang.zhcw.com/lishishuju/jsp/ssqInfoList.jsp?czId=1" id="frmdetail" name="frmdetail" width="100%" frameborder="0" onload="this.height = 20+document.getElementById('frmdetail').contentDocument.body.offsetHeight; delNode('loading');"></iframe>
进入这个网址,是一个纯净的数据表HTML,赛高~
再次点击查询,观察XHR,仍然没有数据……
调整到All,原来它是直接跳转的网页而不是异步刷新数据:
那么,又要解析html了………………
2、分析数据
仍然是标准的html表格:
<tbody>
<tr>
<td>1</td>
<td>2018-08-09</td>
<td>2018092</td>
<td class="kaiHao">06 10 16 19 24 33 <span>16</span></td>
<td>318,819,890</td>
<td>1</td>
<td>10,000,000</td>
<td>95</td>
<td>269,198</td>
<td>882</td>
<td>3,000</td>
<td>921,043,817</td>
</tr>
<tr>
<td>2</td>
<td>2018-08-07</td>
<td>2018091</td>
<td class="kaiHao">06 11 13 17 25 32 <span>07</span></td>
<td>315,853,082</td>
<td>4</td>
<td>8,999,907</td>
<td>81</td>
<td>246,907</td>
<td>1107</td>
<td>3,000</td>
<td>854,322,188</td>
</tr>
</tbody>
这里要注意:
(1)设计了分页,所以表面上是三个参数:
参数名 | 参数值 |
---|---|
czId | 1 |
beginIssue | 2018091 |
endIssue | 2018092 |
实际上还有一个参数:
参数名 | 参数值 |
---|---|
currentPageNum | 1 |
(2)蓝球是在span标签里面的,而不是td
(3)数字依然有逗号分隔
3、开始爬取
首先需要处理翻页的问题,观察到下一页翻到最后尾页是相同的,没有价值,我们可以比对上一页和尾页,如果上一页和尾页的差距在一页以上,就说明没爬完,需要进行翻页。
举个例子,当前页数为1,上一页为0,尾页为2,一共有2页。那么当上一页为0的时候,所在页为1,需要翻页;上一页为1的时候,和尾页差距为1,这时候所在页为2,已经爬取完了。而翻页的方式仅仅需要修改提交请求的参数,很容易写出这样的代码:
class ParserZhcwssq(HTMLParser):
flag_tbody = False
flag_tr = False
flag_td = False
flag_ballnum = False
flag_nextpage = False
linedata = []
result = []
url_nextpage = []
def reset_attr(self):
self.flag_tbody = False
self.flag_tr = False
self.flag_td = False
self.flag_ballnum = False
self.flag_nextpage = False
self.linedata = []
self.result = []
self.url_nextpage = []
def handle_starttag(self, tag, attrs): # start tag
if (str(tag).startswith("tbody")):
self.flag_tbody = True
elif (self.flag_tbody == True):
if (str(tag).startswith("tr")):
self.flag_tr = True
elif (str(tag).startswith("td")):
self.flag_td = True
for k,v in attrs:
if k == 'class' and v == "kaiHao":
self.flag_ballnum = True
elif self.flag_nextpage == True and str(tag).startswith("a"):
self.url_nextpage.append(attrs[0][1].split("PageNum=")[1])
def handle_endtag(self, tag): # end tag
if (self.flag_tbody == True):
if (str(tag).startswith("tr")):
self.result.append(self.linedata)
self.linedata = []
self.flag_tr = False
elif (str(tag).startswith("td")):
self.flag_td = False
self.flag_ballnum = False
elif (str(tag).startswith("tbody")):
self.flag_tbody = False
def handle_data(self, data): # <xx>data</xx>
if (self.flag_td == True):
if self.flag_ballnum == True:
self.linedata.extend(data.rstrip().split(" "))
else:
self.linedata.append("".join(data.split(",")))
def handle_comment(self, data): # <!-->comment<--!>
if(data.strip() == '分页条'):
self.flag_nextpage = True
def parseZhcwssq(html,datapath,htmlurl,method,headers,params):
print("start grabbing...")
parser = ParserZhcwssq()
wmode = 'w'
while(1):
parser.feed(html)
print(parser.result)
with open(BASEPATH + datapath, wmode) as f:
for line in parser.result:
f.write(",".join(line))
f.write("\n")
if int(parser.url_nextpage[1]) != int(parser.url_nextpage[3]) - 1:
print("grabbing nextpage...")
wmode = 'a'
params['currentPageNum'] = parser.url_nextpage[2].strip()
html = getHtml(htmlurl, method, headers,params)
parser.reset_attr()
else:
print("finished.")
break
爬取结果:
总结
1、这次爬取的结果已经上传到github上的lotterydata上面了,可以下载来直接使用。
2、完整的爬取代码上传到了github上的lotteryhistorygrabber,可以下载来学习和实践。
3、数据格式:
lot_500_ssq.txt:
期号,红球1,红球2,红球3,红球4,红球5,红球6,蓝球,快乐星期天,奖金奖池(元),
一等奖注数,一等奖奖金(元),二等奖注数,二等奖奖金(元),总投注额(元),开奖日期
lot_zhcw_ssq.txt:
序号,开奖日期,期号,红球1,红球2,红球3,红球4,红球5,红球6,蓝球,销售额,
一等奖注数,一等奖奖金(元),二等奖注数,二等奖奖金(元),三等奖注数,三等奖奖金(元),奖池(元)
4、这次的爬取总的来说还是十分简单,没有涉及到任何异步加载延迟、复杂的iframe结构、登录cookie/session、js双重加密、服务器反爬虫封IP等等问题,但是可以当做一次基本练习,爬下来的数据也是很有价值的。更加复杂爬虫的推荐使用scrapy+splash等专业爬虫平台来构建项目。talk is cheep,立刻动手,开始编写属于你的爬虫吧~