文章目录

前言

在之前《用Python爬取双色球开奖信息(升级版)》中已经介绍了简单的urllib+re正则的方式来提取每天的双色球数据,当然这是有用的,虽然数据量少,但是可以用来做一些比如“买了股票自动比对中奖情况然后推送”这一类程序或网页。

但这种爬取方式仍然存在问题:容易被网站的反爬虫或者反作弊发现。也就是说,你爬取这些接口,那边的服务器系统会有日志的,并且有自动处理程序,甚至会有机器学习的程序。尽管这种数据没有什么敏感性,根本不会来封你的IP,不过也要养成良好的爬虫习惯,至少——在爬取的时候加个header,不要被一句简单的awk命令就给筛选出来安排得明明白白了(我在实习的时候经常一句awk就筛出那些刷金币刷花接口的小同学,尽管大佬们提供了svm机器学习模型来自动处理)。

注意到有的网站提供了大量的历史开奖数据,比如500彩票网双色球历史数据中彩网双色球历史数据,我们可以都爬下来,然后自行对比使用,一个可能的用途是用机器学习去预测走势(虽然国内的双色球……em……每天停售后两个小时才开奖……可以做很多事……而且如果完全公正了,从概率上来说机器学习学出的中奖概率是一样的……),用来玩玩吧。

运行环境

解释器:python3.5.2

IDE:Pycharm 2016.3.2

浏览器:Chrome

爬取500双色球的历史数据

500彩票网的网页比较规矩,没有什么花里胡哨的东西。

1、进入网页,查看接口

用Chrome浏览器进入500双色球历史数据网页,然后右键检查,翻到NetworkXHR的一栏。

网页上随便选择期数,点击查看,就能看到网页异步获取数据的接口了:

点进去看一看,非常简单的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)中间有一栏是&nbsp,也就是空格,这一栏的体现是一个名为快乐星期天的属性。快乐星期天是指开出两个蓝球,第一个正常开,该中啥中啥;第二个蓝号中了,并且前面红号任意中5个,就能得到固定的3000元奖金。

这里由于只是一个活动,不清楚以后是否会用,而且快乐星期天数据格式有:空格(&nbsp,unicode显示为\xa0);0;1个数字;3个逗号分隔的数字,因此可以考虑把这个属性变为用“-”连接的字符串,如果以后有需要可以自行处理,如果不需要可以直接略过整个属性。

(2)数字采用了银行的那种逗号分隔的格式,这是方便浏览的格式;同时我们想做的也是用逗号分隔,这会产生歧义,并且我们需要的是存储使用而不是观看,所以需要在采集的时候把逗号去掉。

(3)多测几次数据,会发现500没有对数据进行分页,这对爬取来说非常有利。

3、开始爬取

那么,这次我们使用python强大的库吧,这里使用的是用于请求数据的requests以及用于解析HTML的HTMLParser。  

requests的用法非常简单,注意编码格式不要中文乱码即可:

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 
beginIssue2018091
endIssue2018092

实际上还有一个参数:

参数名 参数值 
 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,立刻动手,开始编写属于你的爬虫吧~


转载请注明出处http://www.bewindoweb.com/208.html | 三颗豆子
分享许可方式知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
重大发现:转载注明原文网址的同学刚买了彩票就中奖,刚写完代码就跑通,刚转身就遇到了真爱。
你可能还会喜欢
具体问题具体杠