python requests、sqlalchemy、logging、schedule、ThreadPoolExecutor、OOP
前言
这篇文章讲述使用python requests爬取网页信息、sqlalchemy对数据库增删改查、logging打印日志、schedule进行任务调度、ThreadPoolExecutor线程管理。使用这个项目需要懂得面向对象编程思想,项目名取python-crawler是因为我是围绕这个业务去做的。源码地址,下面按照项目结构层次的顺序作为讲解。
一、项目结构
项目主要分为dao、database、request、service、util层。log、sql是这个项目的一些相关的文件夹
.
├── README.md
├── __init__.py #启动入口
├── dao #对数据库操作的一些文件
│ ├── __init__.py
│ ├── base_dao.py #操作数据库的简单封装
│ ├── eastmoney_dao.py #根据自己的业务对数据源操作
│ └── user_test_dao.py
├── database #连接数据库
│ └── __init__.py
├── log #日志打印的文件
│ └── crawler.log
├── request #请求网页信息
│ ├── __init__.py
│ └── request_util.py #对网页操作的工具
├── service #自己的业务
│ ├── __init__.py
│ └── eastmoney_service.py #获取东方财富的业务
├── sql
│ └── test.sql #数据库脚本
└── util
├── __init__.py
├── my_logger.py #日志
├── my_schedule.py #定时任务
└── my_thread_pool_executor.py #线程池
二、dao层
提示:dao层下面的文件是对数据库的增删改查操作
1. init.py
# coding=utf-8
from dao.eastmoney_dao import *
from dao.user_test_dao import *
# ...导入dao包可以使用的文件方便在一个地方可以直接引用所有可用文件,例如:from dao import *
2. base_dao.py
2.1 代码
# coding=utf-8
import database
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import *
from util.my_logger import log
import random
# 数据库访问对象
class BaseDao:
# 实体对象的基类
BaseEntity = declarative_base()
def __init__(self, entity):
self._entity = entity
# 查询
def base_query(self):
return database.DBSession.query(self._entity)
# 根据id查询一条记录
def base_query_by_id(self, id):
return database.DBSession.query(self._entity).filter_by(id=id)
# 添加 or 编辑
def base_save(self, entity):
# entity如果id主键字段有值会进行编辑,否则进行添加一条新的记录
database.DBSession.add(entity)
database.DBSession.commit()
# 根据id删除一条数据
def base_delete_by_id(self, id):
person = database.DBSession.query(self._entity).filter_by(id=id).first()
database.DBSession.delete(person)
database.DBSession.commit()
# 删除全部数据
def base_delete_all(self):
database.DBSession.query(self._entity).delete()
database.DBSession.commit()
2.2 知识点
- import自己的database得到连接数据库后的DBSession
- import所有对数据库操作的包,其它文件在引用此文件就不需要再次引入
- 继承BaseDao必须传入要操作的数据表对象,这样就可以知道当前是在操作哪个table entity
- 使用sqlalchem封装对数据库操作的方法
- filter_by(
id
=id)在使用这个方法时需要注意红色标注的是数据表的主键字段 - base_save方法是根据当前对象的主键是否有值来判断是添加还是修改。有值会进行编辑,否则进行添加一条新的记录
- 在添加后会主动返回当前主键值
- 删除一条数据时必须先查出来才能进行删除
3. eastmoney_dao.py
3.1 代码
# coding=utf8
from dao.base_dao import *
# eastmoney数据库访问对象
class EastmoneyDao(BaseDao):
def __init__(self, entity):
super().__init__(entity)
# 添加
def add(self, entity):
self.base_save(entity)
log.info('eastmoney_entity add success')
# 删除全部
def delete_all(self):
self.base_delete_all()
log.info('eastmoney_entity delete_all success')
# 数据表
class EastmoneyEntity(BaseDao.BaseEntity):
__tablename__ = "eastmoney_info"
id = Column(Integer, primary_key=true)
title = Column(String(255))
content = Column(String(255))
def __init__(self):
pass
3.2 知识点
- 使用自己封装好的base_dao
- 继承BaseDao对象调用已经实现好的对数据库操作的方法
4. user_test_dao.py
4.1 代码
# coding=utf8
from dao.base_dao import *
# userTest数据库访问对象
class UserTestDao(BaseDao):
def __init__(self, entity):
self.entity = entity
super().__init__(entity)
# 查询
def get(self):
userData = self.base_query()
log.info('\n')
log.info('start query user_test')
if userData is not None:
for item in userData:
log.info(item.id)
log.info(item.name)
log.info('finish query user_test\n')
# 添加
def add(self):
ut = UserTestEntity()
ut.name = random.random()
ut.age = 21
ut.creator_id = 66
self.base_save(ut)
log.info('主键回填:%s', ut.id)
log.info('UserTestEntity add success')
def delete(self):
ut = self.base_query().one()
self.base_delete_by_id(ut.id)
log.info('UserTestEntity 41 delete success')
def delete_all(self):
self.base_delete_all()
log.info('UserTestEntity all delete success')
def edit(self):
ut = self.base_query().one()
ut.name = 'edit hello world'
self.base_save(ut)
log.info('UserTestEntity edit success')
# 数据表实体对象
class UserTestEntity(BaseDao.BaseEntity):
__tablename__ = 'user_test'
# 主键
id = Column(BigInteger, primary_key=True)
# 年龄
age = Column(Integer)
# 名称
name = Column(String(255))
# 创建者id
creator_id = Column(Integer())
def __init__(self):
pass
4.2 知识点
- 使用自己封装好的base_dao
- 继承BaseDao对象调用已经实现好的对数据库操作的方法
- 在使用base_save方法的时候如果是添加,它会主动把当前主键的值返回
- 在使用base_delete_by_id方法需要先查询到这个值再去删除,删除全部也是一样要查询全部在进行删除
三、database层
提示:database层下面的文件是对数据库的连接操作
1. init.py
1.1 代码
# coding=utf-8
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from util.my_logger import log
DBSession = None # 创建一个操作数据库的会话
def get_session(user, password, host, port, db):
try:
if DBSession is None:
url = 'mysql+pymysql://{}:{}@{}:{}/{}'.format(user, password, host, port, db)
log.info(url)
log.info("开始连接数据库...")
# engine = create_engine(url, echo=True)
engine = create_engine(url)
log.info("数据库连接成功...")
session = sessionmaker(bind=engine)
log.info("会话已创建")
return session()
else:
return DBSession
except Exception as e:
log.error("数据库连接失败...")
log.error(e.args)
1.2 知识点
- 声明一个DBSession赋值连接数据库之后的session
- 提供一个连接数据库的方法得到session
- 使用pymysql,要使用python安装的
四、request层
提示:request层下面的文件是对requests库的封装
1. request_util.py
1.1 代码
# coding=utf8
import requests
class RequestUtil:
# 请求连接返回response
def __init__(self):
pass
@staticmethod
def get(url):
if url is None:
return None
# 伪装成一个浏览器
headers = {
'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6'}
# 请求内容
response = requests.get(url,
headers=headers,
params=None)
response.encoding = "UTF-8"
return response
1.2 知识点
- 使用requests库
- 伪装一个浏览器去请求网页得到response网页信息
五、service层
提示:service层是自己实现的业务
1. eastmoney_service.py
提示:这里只会讲解对网页如何解析
1.1 代码
# 调用请求网页工具类
response = RequestUtil.get(url)
html = response.text
# log.info(result2)
# 解析html
soup = bs4.BeautifulSoup(html, 'html.parser')
# 去除不需要的标签
[s.extract() for s in soup(['img', 'iframe', 'video'])]
# 去除标签中不需要的属性
# del soup.a["class"]
# del soup.a["href"]
# divs = soup.find_all().findall('div', __class=re.compile('newsContent')))
h1 = soup.find('div', class_='topbox').find('div')
divs = soup.find('div', class_='infos').findAll('div', class_='item')
time = divs[0]
source = divs[1]
content_body = soup.find('div', class_='abstract').find('div', class_='txt')
# content_body = soup.find('div', class_='abstract').find('div', id=re.compile('ContentBody'))
log.info("标题: %s" % h1)
log.info("时间:%s" % time)
log.info("来源:%s" % source)
1.2 知识点
- 使用自己封装好的RequestUtil类
- 使用
bs4.BeautifulSoup
进行网页解析查找,需要引用import bs4
库 [s.extract() for s in soup(['img', 'iframe', 'video'])]
可以过滤不想要的标签domsoup.find('div', class_='topbox')
第一个参数根据标签查找,第二个参数根据标签绑定的class查找findAll
是返回所有符合筛选的dom
六、util层
提示:util层是一些工具类
1. my_logger.py
1.1 代码
# coding=utf-8
import logging
from logging import handlers
class MyLogger(object):
level_relations = {
'debug': logging.DEBUG,
'info': logging.INFO,
'warning': logging.WARNING,
'error': logging.ERROR,
'crit': logging.CRITICAL
} # 日志级别关系映射
def __init__(self, filename='log/crawler.log', level='debug', when='D', backCount=5,
fmt='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s'):
self.logger = logging.getLogger(filename)
format_str = logging.Formatter(fmt) # 设置日志格式
self.logger.setLevel(self.level_relations.get(level)) # 设置日志级别
sh = logging.StreamHandler() # 往屏幕上输出
sh.setFormatter(format_str) # 设置屏幕上显示的格式
# 往文件里写入,指定间隔时间自动生成文件的处理器
th = handlers.TimedRotatingFileHandler(filename=filename, when=when, backupCount=backCount,
encoding='utf-8')
# 实例化TimedRotatingFileHandler
# interval是时间间隔,backupCount是备份文件的个数,如果超过这个个数,就会自动删除,when是间隔的时间单位,单位有以下几种:
# S 秒
# M 分
# H 小时、
# D 天、
# W 每星期(interval==0时代表星期一)
# midnight 每天凌晨
th.setFormatter(format_str) # 设置文件里写入的格式
self.logger.addHandler(sh) # 把对象加到logger里
self.logger.addHandler(th)
log = MyLogger().logger
1.2 知识点
filename='log/crawler.log'
把日志输出到当前项目路径的log文件里level='debug'
日志输出级别when='D'
when是间隔的时间单位fmt
日志输出格式:时间-路径-行数-日志级别:信息
2. my_schedule.py
2.1 代码
# coding=UTF-8
import schedule
# 定时任务
class MySchedule:
def __init__(self):
pass
# 添加一个以秒为单位的任务
@staticmethod
def seconds(interval, job_func):
schedule.every(interval).seconds.do(job_func)
# 添加一个以分钟为单位的任务
@staticmethod
def minutes(interval, job_func):
schedule.every(interval).minutes.do(job_func)
# 添加一个以小时为单位的任务
@staticmethod
def hours(interval, job_func):
schedule.every(interval).hours.do(job_func)
# 取消一个任务
@staticmethod
def cancel_job(job_func):
schedule.cancel_job(job_func)
# 运行等待要执行的任务
def run_schedule():
while True:
schedule.run_pending()
2.2 知识点
import schedule
- 提供几个常用的定时方法
run_schedule()
等待需要执行的定时任务
3. my_thread_pool_executor.py
3.1 代码
from concurrent.futures import ThreadPoolExecutor
# 创建线程池 10线程个数量
pool = ThreadPoolExecutor(max_workers=10)
# 线程
class MyThreadPoolExecutor:
def __init__(self):
pass
# 添加一个线程
@staticmethod
def add(fn):
return pool.submit(fn)
3.2 知识点
from concurrent.futures import ThreadPoolExecutor
使用ThreadPoolExecutormax_workers=10
最大使用10个线程- 提供一个静态方法给其它文件使用
七、启动入口&使用方法
1. 代码
# coding=utf-8
import traceback
from dao import *
from util.my_schedule import *
from util.my_thread_pool_executor import MyThreadPoolExecutor
from service.eastmoney_service import EastmoneyService
if __name__ == '__main__':
try:
# 连接数据库
database.DBSession = database.get_session("root", "root123456", "localhost", "3306", "test") # 连接数据库
# 启动定时任务
future = MyThreadPoolExecutor.add(run_schedule) # 启动定时任务
log.info('start %s', not future.done())
# 删除eastmoney数据
EastmoneyDao(EastmoneyEntity).delete_all()
# 爬取东方财富&添加到数据库
MyThreadPoolExecutor.add(EastmoneyService().get)
# 获取user_test数据
userDao = UserTestDao(UserTestEntity)
userDao.get()
# 删除user_test数据
userDao.delete()
# 添加user_test数据
userDao.add()
# 编辑user_test数据
userDao.edit()
log.info('结束以上方法调用...')
except Exception as e:
log.error(e)
log.error("\n" + traceback.format_exc())
2. 知识点
- 调用
database.get_session()
得到一个session赋值给database.DBSession
,这样其它操作数据库的文件只需要使用database.DBSession
就可以完成对数据库操作 - 创建一个线程启动定时任务
MyThreadPoolExecutor.add(run_schedule)
这样这个服务也会一直运行,run_schedule
就是my_schedule.py里的方法 log.info('start %s', not future.done())
检查线程运行成功后就可以执行自己的业务了import traceback
查看异常的轨迹,可以看从哪个地方报的异常
总结
本项目是根据解析网页业务去拓展出来的,是通过这个业务我又去了解如何使用数据库、定时任务、线程、日志等一些功能。我使用面向对象编程是为了可以更好的封装代码。dao层的BaseDao加上database层DBSession就可以完整的实现连接数据库和操作数据库,这里还是可以继续封装的,复制到其它项目里只需要把库引入好就可以实现一些简单的数据库操作。其余的代码都是一些简单的使用,还没有进行一个很好的封装。