python爬虫之双色球所有历史中奖信息

这篇Blog主要介绍爬取 www.500.com 网站中所有双色球的历史开奖即中奖情况信息

  首先分析网页的整体分布,和定制好需要爬取的信息。重中之重,一定明确爬取信息需求,这个不仅关系到后面的程序设计,还有可能因为一些并不需要的信息加大了爬取难度及持久化操作(我就因为一些不必要需要在信息刷选爬取过程中遇到大问题)。
python爬虫之双色球所有历史中奖信息_第1张图片
  这个页面包含了我们所需有爬取的信息,在这里我准备爬取的信息有期数、开奖日期、开奖号码、本期销量、奖池滚动和中奖注数。起初我还想按奖项把内容爬取下来,然后用字典把每个奖项的中奖注数和单注奖金存储,后面刚开始没有进行持久化操作,运行程序就爆炸了,想想2000多个网页,内存肯定爆炸,我起初没注意是这个问题造成内存泄露问题,还一直以为是循环使用对象了,还以看了许久python的垃圾回收。总之这个问题导致我程序爬取到400多个请求的时候导致进程被系统杀死,吃一堑长一智啊!避免无限向系统申请内存,超大列表或字典会把程序搞死的。另外后面设计数据库以及分析爬取数据都是很麻烦的事,所以定制好可行科学的需求。
  好了,言归正传,既然明白了自己的需求下面开始分析,查看上角下拉框,点开几期查看,你会发现网站数据==不是动态生成的==!!!那你的分析工作就会轻松很多,因为这个相当于爬取静态网站的内容!
  通过观察每个网页你会发现它的Url组成是有规律的:每个网页只有数字不一样,正好这些数字就是你要查询的期数,然后很容易就想到用循环创建这些Url,然后用数列存储起来,后面调用就行,但是这样考虑问题就会导致后面get请求时,老是到03089期后面报错,其实这个并不是请求问题,而是根本就没有03090以及03开头后面的内容,这五位数的含义是,前两个代表年份,后面三个代表期数,所以用上面的方法来获取肯定会报错。后来自己查看网页源码,发现竟然有所有期数,所以可以直接爬取,不要去重后处理数据了,直接爬取,join就行,代码如下:

def GetUrls(self):
    """收集所有子页面的url"""
    baseUrlHead = "http://kaijiang.500.com/shtml/ssq/"
    baseUrlEnd = ".shtml"
    html = 'http://kaijiang.500.com/ssq.shtml'
    htmlSource = requests.get(html).content.decode('gbk')
    Selector=etree.HTML(htmlSource)
    UrlMid = Selector.xpath('//*[@class="iSelectList"]/a/text()')
    UrlMid.reverse
    for baseUrlMid in UrlMid:
        #以下两种方式都能链接,但是在数量变大的时候,join方法效率更高,推荐使用,join方法操作的是可迭代对象!!
        url = ''.join([baseUrlHead,baseUrlMid,baseUrlEnd])
        # url = baseUrlHead + baseUrlMid +baseUrlEnd  
        self.Urls.append(url)

  这里我采用xpath分析网页,其实还可以用Beautiful4库进行分析,但是xpath效率还有使用起来会更高效,推荐使用!

信息爬取
  已经获取所有网页的url,下面进行信息筛选和爬取,这个这边就不赘述了,就是基本操作,代码如下:

def GetInfo(self htmlSource):
    """爬取信息"""
    Selector = etree.HTML(htmlSource)#转换为xpath能查询的文本
    #获取期数、
    term = Selector.xpath('//td[@class="td_title01"]/span/a/font/strong/text()')[0] 
    #获取开奖日期
    date = str(Selector.xpath('//td[@class="td_title01"]/span/text()')[1])
    date = date.split(' ')[1].split(':')[1]  #获取开奖日期,注意其中:是中文符号的
    #获取开奖号码
    num = Selector.xpath('//div[@class="ball_box01"]/ul/li/text()')
    num = " ".join(num)      #连接获取中奖号码
    #获取当前销量和滚动奖池
    money = Selector.xpath('//table[@class="kj_tablelist02"]/tr/td/span/text()')      #销售量为下标为2, 奖池下标为3 
    saleMoney = str(''.join(money[2].split(',')).partition('元')[0])
    jackpot = str(''.join(money[3].split(',')).partition('元')[0])
    #在并发处理的时候,可能会会出现下标越界问题,不知道是不是因为requests线程不安全还是啥的,数据会丢失,所以得重新收集销量信息
    Prize = []
    for i in range(3,9):
        prize = Selector.xpath('//table[@class="kj_tablelist02"]/tr[%d]/td/text()'%i)
        if i == 3:
            try:
                #格式化数据
                num_ =  prize[6].replace('\r\n\t\t\t\t','')
                prize[7].replace('\r\n\t\t\t\t','')
            except:
                num_ =  prize[5].replace('\r\n\t\t\t\t','')
            if num_.isdigit():
                pass
            else:
                num_ = 0
        else:
            num_ =  prize[1].replace('\r\n\t\t\t\t','')
        Prize.append(num_)
    #列表用以后面数据持久化操作时循环取出

  最后进行在测试的时候会发现老是发生ConnectError异常,这是因为单个IP频繁快速网页时会对服务器造成负担,所以服务器会拒绝该ip的访问。为了解决这个问题,我开始是设定了阿里云的DNS,以为解析速度加快肯定会有缓解connecterror,但治标不治本,最后添加了代理池,随机ip访问,终于根治这个问题。

proxy = [
        {'https':'https://183.30.204.252:9000'},
        {'https':'https://183.30.204.252:9999'},
        {'https':'https://222.186.15.232:63229'},
        {'https':'https://119.27.177.169:80'},
        {'https':'https://183.129.207.73:14823'},
        {'https':'https://221.217.49.196:9000'},
    ]

  这个代理IP可以上西刺免费代理IP 获取,怎么使用进程池网上也有很多教程,如果有问题可以进行交流。
还有就是插入数据库了,数据库设计很简单,就是按照上面获取的数据进行设计就好,其中因为可能出现不中奖出现 – ,所以设定中奖注数全为string类型了,当然可以选择添加筛选功能,把没有中奖注数的改为0就行。

sql_insert = "INSERT INTO lottery(term,date,num,saleMoney,jackpot,prize1,prize2,prize3,prize4,prize5,prize6)VALUES('%d','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s')"%(int(term),str(date),num,saleMoney,jackpot,Prize[0],Prize[1],Prize[2],Prize[3],Prize[4],Prize[5])
Db.insert(sql_insert)

#其中Db模块是我自己写的对mysql数据库操作的库

  下面就整合以一下代码

from lxml import etree
import requests 
import time
import random 
import threading
import Db
from multiprocessing import Process,Pool
class Spider(object):
"""爬取彩票的历史开奖结果"""
def __init__(self):
    self.Urls = []   #收集的地址
    self.Htmls = []  #收集html文本


def GetUrls(self):
    """收集所有子页面的url"""
    baseUrlHead = "http://kaijiang.500.com/shtml/ssq/"
    baseUrlEnd = ".shtml"
    html = 'http://kaijiang.500.com/ssq.shtml'
    htmlSource = requests.get(html).content.decode('gbk')
    Selector=etree.HTML(htmlSource)
    UrlMid = Selector.xpath('//*[@class="iSelectList"]/a/text()')
    UrlMid.reverse
    for baseUrlMid in UrlMid:
        #以下两种方式都能链接,但是在数量变大的时候,join方法效率更高,推荐使用,join方法操作的是可迭代对象!!
        url = ''.join([baseUrlHead,baseUrlMid,baseUrlEnd])
        # url = baseUrlHead + baseUrlMid +baseUrlEnd  
        self.Urls.append(url)


def GetHtml(self, index, proxy_):
    """获取页面html"""
    #可以直接使用for循环网址,这里选择使用这个主要enumerate有lazy性,只有当是用的时候才会获取这个值,说白了就是生成器。它返回的是索引和值
    print(proxy_)
    flag = len(test.Urls)
    if index == flag:     #判断下标防止越界
        start = 2100
        end = flag
    else:
        start = index - 300
        end = index
    for i,html in enumerate(self.Urls):
        if start-1 < i < end:
            # response = None
            try:
                print(i)
                #设置重连次数
                requests.adapters.DEFAULT_RETRIES = 5
                # s = requests.session()
                #设置连接状态为false
                # s.keep_alive = False
                response = requests.get(html, timeout=(10), proxies=proxy_)
                htmlSource = response.content.decode('gbk')
            except requests.exceptions.ConnectionError:
                print("connection error")
            except requests.exceptions.Timeout:
                print('timeouy')
                continue
            self.GetInfo(i,htmlSource)


def GetInfo(self,htmlSource):
    """爬取信息"""
    Selector = etree.HTML(htmlSource)#转换为xpath能查询的文本
    #获取期数、
    term = Selector.xpath('//td[@class="td_title01"]/span/a/font/strong/text()')[0] 
    #获取开奖日期
    date = str(Selector.xpath('//td[@class="td_title01"]/span/text()')[1])
    date = date.split(' ')[1].split(':')[1]  #获取开奖日期,注意其中:是中文符号的
    #获取开奖号码
    num = Selector.xpath('//div[@class="ball_box01"]/ul/li/text()')
    num = " ".join(num)      #连接获取中奖号码
    #获取当前销量和滚动奖池
    money = Selector.xpath('//table[@class="kj_tablelist02"]/tr/td/span/text()')      #销售量为下标为2, 奖池下标为3 
    saleMoney = str(''.join(money[2].split(',')).partition('元')[0])
    jackpot = str(''.join(money[3].split(',')).partition('元')[0])
    #在并发处理的时候,可能会会出现下标越界问题,不知道是不是因为requests线程不安全还是啥的,数据会丢失,所以得重新收集销量信息
    Prize = [] #存取各个类型的中奖注数
    for i in range(3,9):
        prize = Selector.xpath('//table[@class="kj_tablelist02"]/tr[%d]/td/text()'%i)
        if i == 3:
            try:
                num_ =  prize[6].replace('\r\n\t\t\t\t','')
                prize[7].replace('\r\n\t\t\t\t','')
            except:
                num_ =  prize[5].replace('\r\n\t\t\t\t','')
            if num_.isdigit():
                pass
            else:
                num_ = 0
        else:
            num_ =  prize[1].replace('\r\n\t\t\t\t','')
        Prize.append(num_)
    sql_insert = "INSERT INTO lottery(term,date,num,saleMoney,jackpot,prize1,prize2,prize3,prize4,prize5,prize6)VALUES('%d','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s')"%(int(term),str(date),num,saleMoney,jackpot,Prize[0],Prize[1],Prize[2],Prize[3],Prize[4],Prize[5])
    Db.insert(sql_insert)


if __name__ == '__main__':
    proxy = [
            {'https':'https://183.30.204.252:9000'},
            {'https':'https://183.30.204.252:9999'},
            {'https':'https://222.186.15.232:63229'},
            {'https':'https://119.27.177.169:80'},
            {'https':'https://183.129.207.73:14823'},
            {'https':'https://221.217.49.196:9000'},
        ]
    test = Spider()
    s_time = time.time()
    test.GetUrls()
    time.sleep(10)
    for index in (300,600,900,1200,1500,1800,2100,len(test.Urls)):  #设定增量   
        proxy_ = random.choice(proxy)
        test.GetHtml(index, proxy_)
    e_time = time.time()
    print("爬取用时:", e_time - s_time)
    print(test.count)

python爬虫之双色球所有历史中奖信息_第2张图片
  虽然能够完美爬取网页上信息,但是效率也忒低了吧,爬取2298个网页内容竟然用了2000多秒,不能忍受!!!
  这时候就可以考虑并发爬取了,因为爬取的信息有期数,无需考虑信息是否有序,所以并发爬取都信息处理问题不大,相信细心的 同学已经看到我们上面导入的threading和multiprocessing模快了,下面添加并发编程

if __name__ == '__main__':
    proxy = [
            {'https':'https://183.30.204.252:9000'},
            {'https':'https://183.30.204.252:9999'},
            {'https':'https://222.186.15.232:63229'},
            {'https':'https://119.27.177.169:80'},
            {'https':'https://183.129.207.73:14823'},
            {'https':'https://221.217.49.196:9000'},
        ]
    test = Spider()
    s_time = time.time()
    test.GetUrls()
    p =Pool()
    threads = []
    i = 0
    for index in (300,600,900,1200,1500,1800,2100,len(test.Urls)):  #设定增量
        print("第%d个进程"%(i+1))
        proxy_ = random.choice(proxy)
        # test.GetHtml(index, proxy_)
        t = threading.Thread(target=test.GetHtml, args=(index,proxy_))
        threads.append(t)
        # p.apply_async(test.GetHtml,args=(index,proxy_))
    # p.close()             #关闭进程池
    # p.join()
    for i in range(len(threads)):
        threads[i].start()
    for i in range(len(threads)):
        threads[i].join()
    e_time = time.time()
    print("爬取用时:", e_time - s_time)
    print(test.count)

  很简单的就使用了并发,但在使用的时候出现了一点点问题,因为requests线程不安全,有时候会出现数据丢失,在爬取中奖注数时会出现数据丢失,所以在那进行了一波排错小处理。最后我们就成功从单线程向并发进化了。
下面分析一波效率

爬取方式 耗时(s)
单线程 2260.354
多线程 158.194
多进程 153.767

  这里多进程比多线程慢是开进程耗时所致的吗?后面改成用四个进程和四个线程测试,四进程的会稍微比四线程的快一点,但是相差不大,所以这个程序中随便使用哪种都行。

完整源码已经挂在github上,有需要的同学可以联系我!

你可能感兴趣的:(python爬虫)