您的位置:首页 > 编程语言 > Python开发

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

2018-08-12 03:49 791 查看

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

  首先分析网页的整体分布,和定制好需要爬取的信息。重中之重,一定明确爬取信息需求,这个不仅关系到后面的程序设计,还有可能因为一些并不需要的信息加大了爬取难度及持久化操作(我就因为一些不必要需要在信息刷选爬取过程中遇到大问题)。

  这个页面包含了我们所需有爬取的信息,在这里我准备爬取的信息有期数、开奖日期、开奖号码、本期销量、奖池滚动和中奖注数。起初我还想按奖项把内容爬取下来,然后用字典把每个奖项的中奖注数和单注奖金存储,后面刚开始没有进行持久化操作,运行程序就爆炸了,想想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)


  虽然能够完美爬取网页上信息,但是效率也忒低了吧,爬取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上,有需要的同学可以联系我!

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: