python操作MySQL数据库心得(numpy数组写入数据库)

    最近突发奇想,想把以前用Matlab实现的对高频彩的开奖数据进行抓取并保存到本地的项目重新用python做一遍。加上前段时间学习的MySQL,想将读取回来的开奖数据存放到数据库里试试看。

    废话不多说,实操看看。

网页下载器

这部分就比较简单了,不过值得一提的是,response需要encoding一下才能正常识别出内容,不然就是一大堆的乱码。

import requests
import re
class HtmlDownloader(object):
    def download(self, url):
        '''
        实现对输入网址的数据请求以及处理响应内容并输出
        :param url: 输入网址
        :return: 响应的网页内容
        '''
        if url is None:
            return None
        user_agent = "Chrome/67.0.3396.99"
        headers = {"User-Agent": user_agent}
        signal = False
        index = 0
        # 这里我并没有设置timeout,因为采集数据过程不希望被超时打断,而是设置了while循环,以跳出信号或是连接次数判断跳出
        while not signal:
            try:
                response = requests.get(url, headers=headers)
                response.encoding = "utf-8"
                signal = True
            except:
                print("连接失败,正在重新连接...")
                index += 1
            # 设置了5次重新连接的机会,5次都没有响应,就认倒霉吧,继续下一个页面好了
            if index >= 5:
                return None
        return response.text
    
    def dateChange(self, date):
        '''
        实现对日期格式的转换,输出格式为XXXXXXXX
        :param date: 输入未处理格式的日期
        :return: 输出所需格式的日期
        '''
        reg = re.compile(r"(\d{4})\D?(\d{2})\D?(\d{2})")
        if reg.match(date) is not None:
            result = reg.findall(date)[0]
            return result[0] + result[1] + result[2]
        else:
            return None

    因为要爬取的网页http://caipiao.163.com/award/cqssc/XXXXXXXX.html,其中XXXXXXXX就是日期,例如20180501,所以网页下载器htmldownloader类的其中一个小方法就是日期转换datechange()。

网页解析器

在网页点开F12,点击选取元素发现,在这样的class="start" 的td标签里面包含着两个我们想要的信息,开奖期号以及开奖号码。并且期数是以40为倍数3期3期第走的,得到的数据排列顺序是001,041,081,002,042,082....,所以网页解析器就可以这么写:

这里使用了bs4库中的BeautifulSoup模块,原本soup对象我是直接soup = BeautifulSoup(html_text)的,但是会有写法错误警告,虽然警告并不影响程序正常运行,但是处于强迫症,研究了一番,在后面加一个参数"lxml"就好了。

from bs4 import BeautifulSoup
import numpy as np
class HtmlParser(object):
    def dataGet(self, html_text):
        '''
        解析并获取html_text中所需的内容
        :param html_text: 网页内容
        :return: data:numpy数组格式的数据,每个维度分别是开奖期号以及5个开奖号码
        '''
        if html_text is None:
            return None
        soup = BeautifulSoup(html_text, "lxml")
        periods = soup.find_all('td', class_="start")
        # per和data列表分别存储期号和开奖号
        data = []
        per = []
        for period in periods:
            per.append(period.get("data-period"))
            data.append(period.get("data-win-number"))
        # 如果data的长度为0,那就没有继续往下处理的必要了,直接return None
        if len(data) == 0:
            return None
        # 把per和data交由专门对数据进行处理的私有方法去完成,将输出的内容return
        return self._dataHandle(per, data)

    def _dataHandle(self, periods, datas):
        '''
        对数据进行处理,该方法绑定dataget方法使用
        :param periods: 期数
        :param datas: 开奖号
        :return: 处理后的numpy数组
        '''
        # 网页中的数据共120条,分3列,每列40个,读取到的开奖期号排列为001,041,081,002,042,082...以此类推
        # 还没开,跳开,漏开的那一期对应的数据,会是一个空字符串或者是None,这些我们要跳过它们
        jump = 3
        tmp = []
        for index in range(3):
            for i in range(40):
                if datas[i * jump + index] is None or datas[i * jump + index] == "":
                    continue
                # 数据是一个以空格分隔的5个数字,所以要用split方法把5个数字切出来放到列表中
                data = datas[i * jump + index].split(" ")
                tmp.append([int(periods[i * jump + index]), int(data[0]), int(data[1]), int(data[2]), int(data[3]),
                            int(data[4])])
        return np.array(tmp)

数据存储器

    这里使用了pymysql库

import pymysql
class DataMemorizer(object):
    def __init__(self):
        self.database = pymysql.connect("localhost", "root", "root", "cqssc")
        self.cur = self.database.cursor()

    def createTable(self, year):
        '''
        在数据库中创建新表
        :param year: 年份,表的命名格式为“year+年份”,如year2018
        :return: 
        '''
        table_name = "year%s" % year
        table = self._toChar(table_name)
        if self._hasThisTable(table_name):
            # 如果这个表已存在,直接返回
            return table_name
        # 主键字段dates,为日期,存放格式为XXXX的月日4位字符串
        # reward存放当天一整天的开奖数据,是一个120*6的数组,数据类型设置为longblob,其实我觉得blob也可以
        sql = "create table " + table + "(dates char(4) not null primary key, reward longblob not null) ENGINE=myisam DEFAULT CHARSET=utf8;"
        self.cur.execute(sql)
        self.database.commit()
        print("创建新表:%s..." % table_name)
        return table_name

    def insertData(self, table_name, dateID, data):
        '''
        向数据库插入数据
        :param table_name:表名 
        :param dateID: 主键值
        :param data: 数据
        :return: 
        '''
        table = self._toChar(table_name)
        date = self._toStr(dateID)
        if self._hasThisId(table_name, dateID):
            # 如果这个ID已经存在,跳转到修改数据方法
            self.updateData(table_name, dateID, data)
        else:
            # 先要将numpy数组转换成二进制流,才能存到数据库中
            b_data = data.tostring()
            sql = "insert into " + table + " values(" + date + ", %s);"
            self.cur.execute(sql, (b_data,))
            self.database.commit()
            print("已插入数据:%s." % dateID)

    def updateData(self, table_name, dateID, data):
        '''
        更新数据库数据
        :param table_name:表名 
        :param dateID: 主键值
        :param data: 需要修改成的数据
        :return: 
        '''
        table = self._toChar(table_name)
        date = self._toStr(dateID)
        if not self._hasThisId(table_name, dateID):
            # 如果没有这个主键值,那还改个屁,直接返回
            return
        # 同样也是将data这个numpy数组转换一下成二进制流数据
        b_data = data.tostring()
        sql = "update " + table + " set reward = %s where dates = %s;"
        self.cur.execute(sql, (b_data, date))
        self.database.commit()
        print("已更新数据:%s..." % dateID)

    def _hasThisTable(self, table_name):
        '''
        判断是否存在此表
        :param table_name:表名 
        :return: True  or  False
        '''
        sql = "show tables;"
        self.cur.execute(sql)
        results = self.cur.fetchall()
        for r in results:
            if r[0] == table_name:
                return True
        else:
            return False

    def _hasThisId(self, table_name, dateID):
        '''
        判断在此表中是否已经有此主键
        :param table_name: 表名
        :param dateID: 主键值
        :return: True  or  False
        '''
        sql = "select dates from " + table_name + ";"
        self.cur.execute(sql)
        ids = self.cur.fetchall()
        for i in ids:
            if i[0] == dateID:
                return True
        else:
            return False

    def _toChar(self, string):
        '''
        为输入的字符串添加一对反引号,用于表名、字段名等对关键字的规避
        :param string: 
        :return: 
        '''
        return "`%s`" % string

    def _toStr(self, string):
        '''
        为输入的字符串添加一对单引号,用于数值处理,规避字符串拼接后原字符串暴露问题
        :param string: 
        :return: 
        '''
        return "'%s'" % string

    def __del__(self):
        '''
        临走之前记得关灯关电关空调,还有关闭数据库资源
        :return: 
        '''
        self.database.close()

这里就有个地方磨了我很久,关于python对MySQL数据库写入二进制数据,存在以下这么些问题。在这里我创建了一个year2019的表用于演示。

首先,我要写入的是numpy数组,自然要先将numpy转换为二进制数据,那简单,用  .tostring()方法就行

# 1.导入模块
import pymysql
import numpy as np
# 2.连接数据库
db = pymysql.connect('localhost', 'root', 'root', 'cqssc')
# 3.创建游标
csr = db.cursor()
# 4.执行操作
arr = np.array([1, 2, 3, 4, 5])
b_arr = arr.tostring()
csr.execute("insert into year2019 values('1234', %s);", (b_arr))
db.commit()
# 5.打印结果
print(b_arr, type(b_arr))
# 6.关闭连接
db.close()

测试运行结果

扫描二维码关注公众号,回复: 3884906 查看本文章

没有报错,到数据库里看看

这难道是西行取经的加上白龙马之后的师徒五人吗?没道理,那就再读出来看看吧

csr.execute("select reward from year2019 where dates='1234';")

c_arr = csr.fetchone()[0]

print(c_arr, type(c_arr))

既然可以读出来,那就这样吧哈哈哈,我一开始做的时候mysql可没有这么听话,各种报语法错误,算了,做出来就行。

数据获取器

    这个数据获取器在这里就只贴上来其中一个功能,就是获取数据库中已有数据的最后一天的日期。其他的方法当然包含将数据库的数据取出来的还有其他的,但这里重点讲获取最后日期的方法。

import pymysql
import numpy as np
class DataGetter(object):
    def __init__(self):
        # 连接数据库,创建游标
        self.database = pymysql.connect("localhost", "root", "root", "cqssc")
        self.cur = self.database.cursor()

    def getEndDate(self):
        '''
        获取目前已有数据的最后一天的日期
        防止一段时间不爬取存在数据缺失
        :return: 存在数据的最后一天
        '''
        # 先看一下所有表
        sql = "show tables;"
        self.cur.execute(sql)
        tables = self.cur.fetchall()
        length = len(tables)
        if length == 0:
            return None
        # 通过表名后四位的数字,得到最大年份
        for i in range(length):
            if i == 0:
                maxyear = int(tables[i][0][4:])
                continue
            if int(tables[i][0][4:]) > maxyear:
                maxyear = int(tables[i][0][4:])
        while True:
            # 这里try是为了防止下面maxyear -= 1减到表名不存在而报错,表名不存在当然就是返回None
            try:
                table_name = "year" + str(maxyear)
                sql = "select dates from " + table_name + ";"
                self.cur.execute(sql)
            except:
                return None
            values = self.cur.fetchall()
            length = len(values)
            if length > 0:
                # 设置while True是为了防止第一次读取,有表但是数据为空,所以当有数据,就break
                break
            else:
                maxyear -= 1

        for i in range(length):
            if i == 0:
                maxday = int(values[i][0])
                continue
            if int(values[i][0]) > maxday:
                maxday = int(values[i][0])
        return str(maxyear) + self._dayToStr(maxday)
    def _dayToStr(self,day):
        day = str(day)
        if len(day) == 4:
            return day
        return "0" + day

    处理过程比较冗杂,具体思路是先show tables,查看数据库中现有表名,去掉命名格式前面的year后int一下比较大小,取出最大的年份,然后读取那一年的表中的数据,如果没有数据,年数减一,如果在年数递减的过程中,出现不存在该表的情况,直接返回None,读取到数据的话就在读取到的数据中继续找出最大的一天,从而获得拥有数据的最后一天的日期并return返回。目的是为了在一段时间之后再次爬取,不会重复爬取过多的内容。

爬虫调度器

就是调度各单位工作的副总经理啦,责任巨大

from dataMemorizer.dataMemorizer import DataMemorizer
from htmlDownloader.htmlDownloader import HtmlDownloader
from htmlParser.htmlParser import HtmlParser
from dataGetter.dataGetter import DataGetter
import datetime

class SpiderMan(object):
    def __init__(self):
        self.memorizer = DataMemorizer()
        self.downloader = HtmlDownloader()
        self.parser = HtmlParser()
        self.getter = DataGetter()
    def crawlS(self):
        endday = self.getter.getEndDate()
        self._longCatch(endday)

    def _longCatch(self, enddate=None):
        '''
        120期全数据抓取
        :param enddate: 存在数据的最后一天的日期
        :return:
        '''
        if enddate is not None:
            enddate = self.downloader.dateChange(enddate)
        date = datetime.datetime.now()
        datenow = ""
        # 因为这里datenow还不是正确的格式,所以就算日期相同,也可以往while循环里面进去一次,多更新一天的数据总是好的
        index = 0
        while not datenow == enddate:
            datenow = date - datetime.timedelta(days=index)
            datenow = datenow.strftime('%Y%m%d')
            print("正在处理日期:%s......" % datenow)
            url = r"http://caipiao.163.com/award/cqssc/%s.html" % datenow
            html = self.downloader.download(url)
            arr = self.parser.dataGet(html)
            index += 1
            # 每年中都会存在一段时间都不开的情况,如新年,貌似每年有那么8天是不开奖的
            # 所以,为了防止误判,专门判断在无数据的情况下,年份如果小于2000年,那也没必要再往下爬数据了。
            if len(arr) == 0:
                if int(datenow[0:4]) <= 2000:
                    print("当日无数据且年份小于2000,停止执行")
                    break
                else:
                    print("当日无数据,日期为%s,请查证,正在跳过此日数据..." % datenow)
                    continue
            table_name = self.memorizer.createTable(datenow[0:4])
            self.memorizer.insertData(table_name, datenow[4:], arr)
    def __del__(self):
        '''
        总是要关灯关电关空调的,通过这个来间接性关闭数据库资源
        :return:
        '''
        del self.memorizer
if __name__ == "__main__":
    spider = SpiderMan()
    signal = spider.crawl()
    del spider

    大概就是这样了,中间就数据库存储二进制数据的时候,因为各种语法上的错误卡了我好长时间。写个博客也写了一晚上,真失败。希望以后写的程序会越写越有干货。

【如有语法错误,欢迎在评论区纠正】

猜你喜欢

转载自blog.csdn.net/RunffyCSDN/article/details/81486880