7.10
1.多线程
- 程序需要维护许多共享的状态(尤其是可变状态),Python中的列表、字典、集合都是线程安全的,所以使用线程而不是进程维护共享状态的代价相对较小。
- 程序会花费大量时间在I/O操作上,没有太多并行计算的需求且不需占用太多的内存。
2.多进程
- 程序执行计算密集型任务(如:字节码操作、数据处理、科学计算)。
- 程序的输入可以并行的分成块,并且可以将运算结果合并。
- 程序在内存使用方面没有任何限制且不强依赖于I/O操作(如:读写文件、套接字等)。
3.异步编程:对于一次I/O操作(以读操作为例),数据会先被拷贝到操作系统内核的缓冲区中,然后从操作系统内核的缓冲区拷贝到应用程序的缓冲区(这种方式称为标准I/O或缓存I/O,大多数文件系统的默认I/O都是这种方式),最后交给进程。
- 阻塞 I/O(blocking I/O):进程发起读操作,如果内核数据尚未就绪,进程会阻塞等待数据直到内核数据就绪并拷贝到进程的内存中。
- 非阻塞 I/O(non-blocking I/O):进程发起读操作,如果内核数据尚未就绪,进程不阻塞而是收到内核返回的错误信息,进程收到错误信息可以再次发起读操作,一旦内核数据准备就绪,就立即将数据拷贝到了用户内存中,然后返回。
- 多路I/O复用( I/O multiplexing):监听多个I/O对象,当I/O对象有变化(数据就绪)的时候就通知用户进程。多路I/O复用的优势并不在于单个I/O操作能处理得更快,而是在于能处理更多的I/O操作。
- 异步 I/O(asynchronous I/O):进程发起读操作后就可以去做别的事情了,内核收到异步读操作后会立即返回,所以用户进程不阻塞,当内核数据准备就绪时,内核发送一个信号给用户进程,告诉它读操作完成了。
4.编写一个处理用户请求的服务器程序:
- 每收到一个请求,创建一个新的进程,来处理该请求;
- 每收到一个请求,创建一个新的线程,来处理该请求;
- 每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求
7.11(Tornado框架)
1.最适合用来开发需要处理长连接和应对高并发的Web应用
2.例子tornadoweb.org
3.创建并激活虚拟环境
mkdir hello-tornado
cd hello-tornado
python3 -m venv venv
source venv/bin/activate
4.安装Tornado
pip3 install tornado
5.编写Web应用
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write('<h1>Hello, world!</h1>')
def main():
app = tornado.web.Application(handlers=[(r'/', MainHandler), ])
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
if __name__ == '__main__':
main()
6.运行并访问应用
python3 example01.py
注意这里端口成了8888
7.指定Web应用的监听端口
import tornado.ioloop
import tornado.web
from tornado.options import define, options, parse_command_line
# 定义默认端口
define('port', default=8000, type=int)
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write('<h1>Hello, world!</h1>')
def main():
parse_command_line()
app = tornado.web.Application(handlers=[(r'/', MainHandler), ])
app.listen(options.port)
tornado.ioloop.IOLoop.current().start()
if __name__ == '__main__':
main()
8.路由解析
py文件:
import os
import random
import tornado.ioloop
import tornado.web
from tornado.options import define, options, parse_command_line
# 定义默认端口
define('port', default=8000, type=int)
class SayingHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def get(self):
sayings = [
'世上没有绝望的处境,只有对处境绝望的人',
'人生的道路在态度的岔口一分为二,从此通向成功或失败',
'所谓措手不及,不是说没有时间准备,而是有时间的时候没有准备',
'那些你认为不靠谱的人生里,充满你没有勇气做的事',
'在自己喜欢的时间里,按照自己喜欢的方式,去做自己喜欢做的事,这便是自由',
'有些人不属于自己,但是遇见了也弥足珍贵'
]
# 渲染index.html模板页
self.render('index.html', message=random.choice(sayings))
class WeatherHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def get(self, city):
# Tornado框架会自动处理百分号编码的问题
weathers = {
'北京': {'temperature': '-4~4', 'pollution': '195 中度污染'},
'成都': {'temperature': '3~9', 'pollution': '53 良'},
'深圳': {'temperature': '20~25', 'pollution': '25 优'},
'广州': {'temperature': '18~23', 'pollution': '56 良'},
'上海': {'temperature': '6~8', 'pollution': '65 良'}
}
if city in weathers:
self.render('weather.html', city=city, weather=weathers[city])
else:
self.render('index.html', message=f'没有{city}的天气信息')
class ErrorHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def get(self):
# 重定向到指定的路径
self.redirect('/saying')
def main():
"""主函数"""
parse_command_line()
app = tornado.web.Application(
# handlers是按列表中的顺序依次进行匹配的
handlers=[
(r'/saying/?', SayingHandler),
(r'/weather/([^/]{2,})/?', WeatherHandler),
(r'/.+', ErrorHandler),
],
# 通过template_path参数设置模板页的路径
template_path=os.path.join(os.path.dirname(__file__), 'templates')
)
app.listen(options.port)
tornado.ioloop.IOLoop.current().start()
if __name__ == '__main__':
main()
html文件:
mkdir templates
cd templates
touch index.html
vim index.html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tornado基础</title>
</head>
<body>
<h1>{{message}}</h1>
</body>
</html>
touch weather.html
vim weather.html
<!-- weather.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tornado基础</title>
</head>
<body>
<h1>{{city}}</h1>
<hr>
<h2>温度:{{weather['temperature']}}摄氏度</h2>
<h2>污染指数:{{weather['pollution']}}</h2>
</body>
</html>
运行后访问127.0.0.1:8000/saying
和127.0.0.1:8000/weather/北京
等即可看到效果
9.请求处理器
py文件
import os
import re
import tornado.ioloop
import tornado.web
from tornado.options import define, options, parse_command_line
# 定义默认端口
define('port', default=8000, type=int)
users = {}
class User(object):
"""用户"""
def __init__(self, nickname, gender, birthday):
self.nickname = nickname
self.gender = gender
self.birthday = birthday
class MainHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def get(self):
# 从Cookie中读取用户昵称
nickname = self.get_cookie('nickname')
if nickname in users:
self.render('userinfo.html', user=users[nickname])
else:
self.render('userform.html', hint='请填写个人信息')
class UserHandler(tornado.web.RequestHandler):
"""自定义请求处理器"""
def post(self):
# 从表单参数中读取用户昵称、性别和生日信息
nickname = self.get_body_argument('nickname').strip()
gender = self.get_body_argument('gender')
birthday = self.get_body_argument('birthday')
# 检查用户昵称是否有效
if not re.fullmatch(r'\w{6,20}', nickname):
self.render('userform.html', hint='请输入有效的昵称')
elif nickname in users:
self.render('userform.html', hint='昵称已经被使用过')
else:
users[nickname] = User(nickname, gender, birthday)
# 将用户昵称写入Cookie并设置有效期为7天
self.set_cookie('nickname', nickname, expires_days=7)
self.render('userinfo.html', user=users[nickname])
def main():
"""主函数"""
parse_command_line()
app = tornado.web.Application(
handlers=[
(r'/', MainHandler), (r'/register', UserHandler)
],
template_path=os.path.join(os.path.dirname(__file__), 'templates')
)
app.listen(options.port)
tornado.ioloop.IOLoop.current().start()
if __name__ == '__main__':
main()
html文件:
touch templates/userform.html
vim templates/userform.html
<!-- userform.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tornado基础</title>
<style>
.em { color: red; }
</style>
</head>
<body>
<h1>填写用户信息</h1>
<hr>
<p class="em">{{hint}}</p>
<form action="/register" method="post">
<p>
<label>昵称:</label>
<input type="text" name="nickname">
(字母数字下划线,6-20个字符)
</p>
<p>
<label>性别:</label>
<input type="radio" name="gender" value="男" checked>男
<input type="radio" name="gender" value="女">女
</p>
<p>
<label>生日:</label>
<input type="date" name="birthday" value="1990-01-01">
</p>
<p>
<input type="submit" value="确定">
</p>
</form>
</body>
</html>
touch templates/userinfo.html
vim templates/userinfo.html
<!-- userinfo.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tornado基础</title>
</head>
<body>
<h1>用户信息</h1>
<hr>
<h2>昵称:{{user.nickname}}</h2>
<h2>性别:{{user.gender}}</h2>
<h2>出生日期:{{user.birthday}}</h2>
</body>
</html>
运行即可,注册后即可见用户信息页(如下图)
7.12(Tornado异步)
1.旧版本
- 添加@tornado.web.asynchronous装饰器:
class AsyncReqHandler(RequestHandler):
@tornado.web.asynchronous
def get(self):
http = httpclient.AsyncHTTPClient()
http.fetch("http://example.com/", self._on_download)
def _on_download(self, response):
do_something_with_response(response)
self.render("template.html")
- 添加@tornado.gen.coroutine装饰器:
class GenAsyncHandler(RequestHandler):
@tornado.gen.coroutine
def get(self):
http_client = AsyncHTTPClient()
response = yield http_client.fetch("http://example.com")
do_something_with_response(response)
self.render("template.html")
- 使用@return_future装饰器:
@return_future
def future_func(arg1, arg2, callback):
# Do stuff (possibly asynchronous)
callback(result)
async def caller():
await future_func(arg1, arg2)
2.aiomysql基于pymysql封装,实现了对MySQL操作的异步化;操作Redis可以使用aioredis;访问MongoDB可以使用motor
3.例子:
- 创建数据库并添加数据
create database hrs default charset utf8;
use hrs;
/* 创建部门表 */
create table tb_dept
(
dno int not null comment '部门编号',
dname varchar(10) not null comment '部门名称',
dloc varchar(20) not null comment '部门所在地',
primary key (dno)
);
insert into tb_dept values
(10, '会计部', '北京'),
(20, '研发部', '成都'),
(30, '销售部', '重庆'),
(40, '运维部', '深圳');
- 查询和新增部门两个操作
import json
import aiomysql
import tornado
import tornado.web
from tornado.ioloop import IOLoop
from tornado.options import define, parse_command_line, options
define('port', default=8000, type=int)
async def connect_mysql():
return await aiomysql.connect(
host='120.77.222.217',
port=3306,
db='hrs',
user='root',
password='123456',
)
class HomeHandler(tornado.web.RequestHandler):
async def get(self, no):
async with self.settings['mysql'].cursor(aiomysql.DictCursor) as cursor:
await cursor.execute("select * from tb_dept where dno=%s", (no, ))
if cursor.rowcount == 0:
self.finish(json.dumps({
'code': 20001,
'mesg': f'没有编号为{no}的部门'
}))
return
row = await cursor.fetchone()
self.finish(json.dumps(row))
async def post(self, *args, **kwargs):
no = self.get_argument('no')
name = self.get_argument('name')
loc = self.get_argument('loc')
conn = self.settings['mysql']
try:
async with conn.cursor() as cursor:
await cursor.execute('insert into tb_dept values (%s, %s, %s)',
(no, name, loc))
await conn.commit()
except aiomysql.MySQLError:
self.finish(json.dumps({
'code': 20002,
'mesg': '添加部门失败请确认部门信息'
}))
else:
self.set_status(201)
self.finish()
def make_app(config):
return tornado.web.Application(
handlers=[(r'/api/depts/(.*)', HomeHandler), ],
**config
)
def main():
parse_command_line()
app = make_app({
'debug': True,
'mysql': IOLoop.current().run_sync(connect_mysql)
})
app.listen(options.port)
IOLoop.current().start()
if __name__ == '__main__':
main()
7.13(WebSocket)
1.特点
- 建立在TCP协议之上,服务器端的实现比较容易
- 与HTTP协议有着良好的兼容性,默认端口是80(WS)和443(WSS),通信握手阶段采用HTTP协议,能通过各种 HTTP 代理服务器(不容易被防火墙阻拦)
- 数据格式比较轻量,性能开销小,通信高效
- 可以发送文本,也可以发送二进制数据
- 没有同源策略的限制,客户端(浏览器)可以与任意服务器通信
2.处理来自WebSocket的请求(tornado.websocket.WebSocketHandler类)
方法 | 作用 |
---|---|
open(*args, **kwargs) | 建立新的WebSocket连接后,Tornado框架会调用该方法,该方法的参数与RequestHandler的get方法的参数类似,这也就意味着在open方法中可以执行获取请求参数、读取Cookie信息这样的操。 |
on_message(message) | 建立WebSocket之后,当收到来自客户端的消息时,Tornado框架会调用该方法,这样就可以对收到的消息进行对应的处理,必须重写这个方法 |
on_close() | 当WebSocket被关闭时,Tornado框架会调用该方法,在该方法中可以通过close_code和close_reason了解关闭的原因 |
write_message(message, binary=False) | 将指定的消息通过WebSocket发送给客户端,可以传递utf-8字符序列或者字节序列,如果message是一个字典,将会执行JSON序列化。正常情况下,该方法会返回一个Future对象;如果WebSocket被关闭了,将引发WebSocketClosedError |
set_nodelay(value) | 默认情况下,因为TCP的Nagle算法会导致短小的消息被延迟发送,在考虑到交互性的情况下就要通过将该方法的参数设置为True来避免延迟 |
close(code=None, reason=None) | 主动关闭WebSocket,可以指定状态码(详见RFC 6455 7.4.1节)和原因 |
3.WebSocket客户端编程
- 创建WebSocket对象
var webSocket = new WebSocket('ws://localhost:8000/ws');
- 编写回调函数
# 如果要绑定多个事件回调函数,可以用addEventListener方法。另外,通过事件对象的data属性获得的数据可能是字符串,也有可能是二进制数据,可以通过webSocket对象的binaryType属性(blob、arraybuffer)或者通过typeof、instanceof运算符检查类型进行判定
webSocket.onopen = function(evt) { webSocket.send('...'); };
webSocket.onmessage = function(evt) { console.log(evt.data); };
webSocket.onclose = function(evt) {};
webSocket.onerror = function(evt) {};
4.项目:Web聊天室
"""
handlers.py - 用户登录和聊天的处理器
"""
import tornado.web
import tornado.websocket
nicknames = set()
connections = {}
class LoginHandler(tornado.web.RequestHandler):
def get(self):
self.render('login.html', hint='')
def post(self):
nickname = self.get_argument('nickname')
if nickname in nicknames:
self.render('login.html', hint='昵称已被使用,请更换昵称')
self.set_secure_cookie('nickname', nickname)
self.render('chat.html')
class ChatHandler(tornado.websocket.WebSocketHandler):
def open(self):
nickname = self.get_secure_cookie('nickname').decode()
nicknames.add(nickname)
for conn in connections.values():
conn.write_message(f'~~~{nickname}进入了聊天室~~~')
connections[nickname] = self
def on_message(self, message):
nickname = self.get_secure_cookie('nickname').decode()
for conn in connections.values():
if conn is not self:
conn.write_message(f'{nickname}说:{message}')
def on_close(self):
nickname = self.get_secure_cookie('nickname').decode()
del connections[nickname]
nicknames.remove(nickname)
for conn in connections.values():
conn.write_message(f'~~~{nickname}离开了聊天室~~~')
"""
run_chat_server.py - 聊天服务器
"""
import os
import tornado.web
import tornado.ioloop
from handlers import LoginHandler, ChatHandler
if __name__ == '__main__':
app = tornado.web.Application(
handlers=[(r'/login', LoginHandler), (r'/chat', ChatHandler)],
template_path=os.path.join(os.path.dirname(__file__), 'templates'),
static_path=os.path.join(os.path.dirname(__file__), 'static'),
cookie_secret='MWM2MzEyOWFlOWRiOWM2MGMzZThhYTk0ZDNlMDA0OTU=',
)
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
<!-- login.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tornado聊天室</title>
<style>
.hint { color: red; font-size: 0.8em; }
</style>
</head>
<body>
<div>
<div id="container">
<h1>进入聊天室</h1>
<hr>
<p class="hint">{{hint}}</p>
<form method="post" action="/login">
<label>昵称:</label>
<input type="text" placeholder="请输入你的昵称" name="nickname">
<button type="submit">登录</button>
</form>
</div>
</div>
</body>
</html>
<!-- chat.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tornado聊天室</title>
</head>
<body>
<h1>聊天室</h1>
<hr>
<div>
<textarea id="contents" rows="20" cols="120" readonly></textarea>
</div>
<div class="send">
<input type="text" id="content" size="50">
<input type="button" id="send" value="发送">
</div>
<p>
<a id="quit" href="javascript:void(0);">退出聊天室</a>
</p>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
$(function() {
// 将内容追加到指定的文本区
function appendContent($ta, message) {
var contents = $ta.val();
contents += '\n' + message;
$ta.val(contents);
$ta[0].scrollTop = $ta[0].scrollHeight;
}
// 通过WebSocket发送消息
function sendMessage() {
message = $('#content').val().trim();
if (message.length > 0) {
ws.send(message);
appendContent($('#contents'), '我说:' + message);
$('#content').val('');
}
}
// 创建WebSocket对象
var ws= new WebSocket('ws://localhost:8888/chat');
// 连接建立后执行的回调函数
ws.onopen = function(evt) {
$('#contents').val('~~~欢迎您进入聊天室~~~');
};
// 收到消息后执行的回调函数
ws.onmessage = function(evt) {
appendContent($('#contents'), evt.data);
};
// 为发送按钮绑定点击事件回调函数
$('#send').on('click', sendMessage);
// 为文本框绑定按下回车事件回调函数
$('#content').on('keypress', function(evt) {
keycode = evt.keyCode || evt.which;
if (keycode == 13) {
sendMessage();
}
});
// 为退出聊天室超链接绑定点击事件回调函数
$('#quit').on('click', function(evt) {
ws.close();
location.href = '/login';
});
});
</script>
</body>
</html>
运行run_chat_server.py
文件,打开127.0.0.1:8888/login
即可,效果如下图:
7.14
项目实战课件被删了,我看了下,接下来的爬虫和机器学习项目实战都被删了。。。
7.15(网络爬虫和相关工具)
1.网络爬虫概念
2.网络爬虫应用领域
3.网络爬虫合法性(Robots协议,全称是“网络爬虫排除标准”,大多数网站都会定义robots.txt文件)
4.Http回顾:
- HTTP请求(请求行+请求头+空行+[消息体])
- HTTP响应(响应行+响应头+空行+消息体)
5.相关工具
- Chrome Developer Tools:谷歌浏览器内置的开发者工具
- POSTMAN:功能强大的网页调试与RESTful请求工具
- HTTPie:命令行HTTP客户端
- BuiltWith:识别网站所用技术的工具
- python-whois:查询网站所有者的工具
- robotparser:解析robots.txt的工具
6.一个简单的爬虫(数据采集(网页下载)、数据处理(网页解析)和数据存储(将有用的信息持久化))
7.步骤:
- 设定抓取目标(种子页面/起始页面)并获取网页
- 当服务器无法访问时,按照指定的重试次数尝试重新下载页面
- 在需要的时候设置用户代理或隐藏真实IP,否则可能无法访问页面
- 对获取的页面进行必要的解码操作然后抓取出需要的信息
- 在获取的页面中通过某种方式(如正则表达式)抽取出页面中的链接信息
- 对链接进行进一步的处理(获取页面并重复上面的动作)
- 将有用的信息进行持久化以备后续的处理
8.从“搜狐体育”上获取NBA新闻标题和链接的爬虫
9.爬虫注意事项
- 处理相对链接。有的时候我们从页面中获取的链接不是一个完整的绝对链接而是一个相对链接,这种情况下需要将其与URL前缀进行拼接(urllib.parse中的urljoin()函数可以完成此项操作)
- 设置代理服务。有些网站会限制访问的区域(例如美国的Netflix屏蔽了很多国家的访问),有些爬虫需要隐藏自己的身份,在这种情况下可以设置使用代理服务器,可以通过urllib.request中的ProxyHandler来为请求设置代理。
- 限制下载速度。如果我们的爬虫获取网页的速度过快,可能就会面临被封禁或者产生“损害动产”的风险(这个可能会导致吃官司且败诉),可以在两次下载之间添加延时从而对爬虫进行限速。
- 避免爬虫陷阱。有些网站会动态生成页面内容,这会导致产生无限多的页面(例如在线万年历通常会有无穷无尽的链接)。可以通过记录到达当前页面经过了多少个链接(链接深度)来解决该问题,当达到事先设定的最大深度时爬虫就不再像队列中添加该网页中的链接了。
- SSL相关问题。在使用urlopen打开一个HTTPS链接时会验证一次SSL证书,如果不做出处理会产生错误提示“SSL: CERTIFICATE_VERIFY_FAILED”,可以通过使用未经验证的上下文和设置全局的取消证书验证两种方式加以解决:
# 使用未经验证的上下文
import ssl
request = urllib.request.Request(url='...', headers={...})
context = ssl._create_unverified_context()
web_page = urllib.request.urlopen(request, context=context)
# 设置全局的取消证书验证
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
7.16(数据采集和解析)
1.每个步骤的第三方库
- 下载数据 - urllib / requests / aiohttp
- 解析数据 - re / lxml / beautifulsoup4(bs4)/ pyquery
- 缓存和持久化 - pymysql / sqlalchemy / peewee/ redis / pymongo
- 生成数字签名 - hashlib
- 序列化和压缩 - pickle / json / zlib
- 调度器 - 进程(multiprocessing) / 线程(threading) / 协程(coroutine)
2.使用requests获取页面
3.四种采集方式
- 使用正则表达式
- 使用XPath和Lxml(BeautifulSoup)
- PyQuery
实例 - 获取知乎发现上的问题链接
from urllib.parse import urljoin
import re
import requests
from bs4 import BeautifulSoup
def main():
headers = {'user-agent': 'Baiduspider'}
proxies = {
'http': 'http://122.114.31.177:808'
}
base_url = 'https://www.zhihu.com/'
seed_url = urljoin(base_url, 'explore')
resp = requests.get(seed_url,
headers=headers,
proxies=proxies)
soup = BeautifulSoup(resp.text, 'lxml')
href_regex = re.compile(r'^/question')
link_set = set()
for a_tag in soup.find_all('a', {'href': href_regex}):
if 'href' in a_tag.attrs:
href = a_tag.attrs['href']
full_url = urljoin(base_url, href)
link_set.add(full_url)
print('Total %d question pages found.' % len(link_set))
if __name__ == '__main__':
main()
7.17(存储数据)
1.存储海量数据(数据持久化的首选方案应该是关系型数据库,如果要存储海量的低价值数据,文档数据库也是不错的选择,MongoDB是文档数据库中的佼佼者)
2.数据缓存(可以使用Redis来提供高速缓存服务)
3.实例 - 缓存知乎发现上的链接和页面代码
from hashlib import sha1
from urllib.parse import urljoin
import pickle
import re
import requests
import zlib
from bs4 import BeautifulSoup
from redis import Redis
def main():
# 指定种子页面
base_url = 'https://www.zhihu.com/'
seed_url = urljoin(base_url, 'explore')
# 创建Redis客户端
client = Redis(host='1.2.3.4', port=6379, password='1qaz2wsx')
# 设置用户代理(否则访问会被拒绝)
headers = {'user-agent': 'Baiduspider'}
# 通过requests模块发送GET请求并指定用户代理
resp = requests.get(seed_url, headers=headers)
# 创建BeautifulSoup对象并指定使用lxml作为解析器
soup = BeautifulSoup(resp.text, 'lxml')
href_regex = re.compile(r'^/question')
# 将URL处理成SHA1摘要(长度固定更简短)
hasher_proto = sha1()
# 查找所有href属性以/question打头的a标签
for a_tag in soup.find_all('a', {'href': href_regex}):
# 获取a标签的href属性值并组装完整的URL
href = a_tag.attrs['href']
full_url = urljoin(base_url, href)
# 传入URL生成SHA1摘要
hasher = hasher_proto.copy()
hasher.update(full_url.encode('utf-8'))
field_key = hasher.hexdigest()
# 如果Redis的键'zhihu'对应的hash数据类型中没有URL的摘要就访问页面并缓存
if not client.hexists('zhihu', field_key):
html_page = requests.get(full_url, headers=headers).text
# 对页面进行序列化和压缩操作
zipped_page = zlib.compress(pickle.dumps(html_page))
# 使用hash数据类型保存URL摘要及其对应的页面代码
client.hset('zhihu', field_key, zipped_page)
# 显示总共缓存了多少个页面
print('Total %d question pages found.' % client.hlen('zhihu'))
if __name__ == '__main__':
main()
7.19(7.18号睡过头了)
1.协程(coroutine)通常又称之为微线程或纤程,它是相互协作的一组子程序(函数)。所谓相互协作指的是在执行函数A时,可以随时中断去执行函数B,然后又中断继续执行函数A。注意,这一过程并不是函数调用(因为没有调用语句),整个过程看似像多线程,然而协程只有一个线程执行。协程通过yield关键字和send()操作来转移执行权,协程之间不是调用者与被调用者的关系。
2.协程的优势:
- 执行效率极高,因为子程序(函数)切换不是线程切换,由程序自身控制,没有切换线程的开销。
- 不需要多线程的锁机制,因为只有一个线程,也不存在竞争资源的问题,当然也就不需要对资源加锁保护,因此执行效率高很多。
3.示例代码和实例(多线程爬取“手机搜狐网”所有页面)
4.AIOHTTP这个第三方库,实现了HTTP客户端和HTTP服务器的功能,对异步操作提供了非常好的支持,官方文档
7.20(解析动态内容)
1.JavaScript逆向工程(只是看了API)
2.使用BeautifulSoup进行错误示范
import requests
from bs4 import BeautifulSoup
def main():
resp = requests.get('https://v.taobao.com/v/content/live?catetype=704&from=taonvlang')
soup = BeautifulSoup(resp.text, 'lxml')
for img_tag in soup.select('img[src]'):
print(img_tag.attrs['src'])
if __name__ == '__main__':
main()
3.使用Selenium
pip3 install selenium
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
def main():
driver = webdriver.Chrome()
driver.get('https://v.taobao.com/v/content/live?catetype=704&from=taonvlang')
soup = BeautifulSoup(driver.page_source, 'lxml')
for img_tag in soup.body.select('img[src]'):
print(img_tag.attrs['src'])
if __name__ == '__main__':
main()
此程序指定使用Chrome浏览器,需要到Selenium的官方网站找到浏览器驱动的下载链接并下载需要的驱动,然后配置环境变量后方可正常运行
7.21
验证码的课件太简单
7.22(Scrapy概述)
1.定义:Scrapy是Python开发的一个非常流行的网络爬虫框架
2.组件:
- Scrapy引擎(Engine):Scrapy引擎是用来控制整个系统的数据处理流程
- 调度器(Scheduler):调度器从Scrapy引擎接受请求并排序列入队列,并在Scrapy引擎发出请求后返还给它们
- 下载器(Downloader):下载器的主要职责是抓取网页并将网页内容返还给蜘蛛(Spiders)
- 蜘蛛(Spiders):蜘蛛是有Scrapy用户自定义的用来解析网页并抓取特定URL返回的内容的类,每个蜘蛛都能处理一个域名或一组域名,简单的说就是用来定义特定网站的抓取和解析规则
- 条目管道(Item Pipeline):条目管道的主要责任是负责处理有蜘蛛从网页中抽取的数据条目,它的主要任务是清理、验证和存储数据。当页面被蜘蛛解析后,将被发送到条目管道,并经过几个特定的次序处理数据。每个条目管道组件都是一个Python类,它们获取了数据条目并执行对数据条目进行处理的方法,同时还需要确定是否需要在条目管道中继续执行下一步或是直接丢弃掉不处理。条目管道通常执行的任务有:清理HTML数据、验证解析到的数据(检查条目是否包含必要的字段)、检查是不是重复数据(如果重复就丢弃)、将解析到的数据存储到数据库(关系型数据库或NoSQL数据库)中
- 中间件(Middlewares):中间件是介于Scrapy引擎和其他组件之间的一个钩子框架,主要是为了提供自定义的代码来拓展Scrapy的功能,包括下载器中间件和蜘蛛中间件
3.数据处理流程(整个数据处理流程由Scrapy引擎进行控制)
- 引擎询问蜘蛛需要处理哪个网站,并让蜘蛛将第一个需要处理的URL交给它
- 引擎让调度器将需要处理的URL放在队列中
- 引擎从调度那获取接下来进行爬取的页面
- 调度将下一个爬取的URL返回给引擎,引擎将它通过下载中间件发送到下载器
- 当网页被下载器下载完成以后,响应内容通过下载中间件被发送到引擎;如果下载失败了,引擎会通知调度器记录这个URL,待会再重新下载
- 引擎收到下载器的响应并将它通过蜘蛛中间件发送到蜘蛛进行处理
- 蜘蛛处理响应并返回爬取到的数据条目,此外还要将需要跟进的新的URL发送给引擎
- 引擎将抓取到的数据条目送入条目管道,把新的URL发送给调度器放入队列中
4.安装scrapypip3 install scrapy
5.使用scrapy
- 在items.py文件中定义字段,字段用来保存数据
import scrapy
class DoubanItem(scrapy.Item):
name = scrapy.Field()
year = scrapy.Field()
score = scrapy.Field()
director = scrapy.Field()
classification = scrapy.Field()
actor = scrapy.Field()
- 在spiders文件夹中编写自己的爬虫
scrapy genspider movie movie.douban.com --template=crawl
import scrapy
from scrapy.selector import Selector
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from douban.items import DoubanItem
class MovieSpider(CrawlSpider):
name = 'movie'
allowed_domains = ['movie.douban.com']
start_urls = ['https://movie.douban.com/top250']
rules = (
Rule(LinkExtractor(allow=(r'https://movie.douban.com/top250\?start=\d+.*'))),
Rule(LinkExtractor(allow=(r'https://movie.douban.com/subject/\d+')), callback='parse_item'),
)
def parse_item(self, response):
sel = Selector(response)
item = DoubanItem()
item['name']=sel.xpath('//*[@id="content"]/h1/span[1]/text()').extract()
item['year']=sel.xpath('//*[@id="content"]/h1/span[2]/text()').re(r'\((\d+)\)')
item['score']=sel.xpath('//*[@id="interest_sectl"]/div/p[1]/strong/text()').extract()
item['director']=sel.xpath('//*[@id="info"]/span[1]/a/text()').extract()
item['classification']= sel.xpath('//span[@property="v:genre"]/text()').extract()
item['actor']= sel.xpath('//*[@id="info"]/span[3]/a[1]/text()').extract()
return item
- 运行爬虫(scrapy crawl movie -o参数来指定文件名,可将数据导成数据导出成JSON、CSV、XML、pickle、marshal等格式)
- 在pipelines.py中完成对数据进行持久化的操作(清理HTML数据,验证爬取的数据;丢弃重复的不必要的内容;将爬取的结果进行持久化操作)
import pymongo
from scrapy.exceptions import DropItem
from scrapy.conf import settings
from scrapy import log
class DoubanPipeline(object):
def __init__(self):
connection = pymongo.MongoClient(settings['MONGODB_SERVER'], settings['MONGODB_PORT'])
db = connection[settings['MONGODB_DB']]
self.collection = db[settings['MONGODB_COLLECTION']]
def process_item(self, item, spider):
#Remove invalid data
valid = True
for data in item:
if not data:
valid = False
raise DropItem("Missing %s of blogpost from %s" %(data, item['url']))
if valid:
#Insert data into database
new_moive=[{
"name":item['name'][0],
"year":item['year'][0],
"score":item['score'],
"director":item['director'],
"classification":item['classification'],
"actor":item['actor']
}]
self.collection.insert(new_moive)
log.msg("Item wrote to MongoDB database %s/%s" %
(settings['MONGODB_DB'], settings['MONGODB_COLLECTION']),
level=log.DEBUG, spider=spider)
return item
- 修改settings.py文件对项目进行配置
BOT_NAME = 'douban'
SPIDER_MODULES = ['douban.spiders']
NEWSPIDER_MODULE = 'douban.spiders'
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.54 Safari/536.5'
ROBOTSTXT_OBEY = True
DOWNLOAD_DELAY = 3
RANDOMIZE_DOWNLOAD_DELAY = True
COOKIES_ENABLED = True
MONGODB_SERVER = '120.77.222.217'
MONGODB_PORT = 27017
MONGODB_DB = 'douban'
MONGODB_COLLECTION = 'movie'
ITEM_PIPELINES = {
'douban.pipelines.DoubanPipeline': 400,
}
LOG_LEVEL = 'DEBUG'
HTTPCACHE_ENABLED = True
HTTPCACHE_EXPIRATION_SECS = 0
HTTPCACHE_DIR = 'httpcache'
HTTPCACHE_IGNORE_HTTP_CODES = []
HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
7.23
Spider的属性和方法
1.name:爬虫的名字。
2.allowed_domains:允许爬取的域名,不在此范围的链接不会被跟进爬取。
3.start_urls:起始URL列表,当我们没有重写start_requests()方法时,就会从这个列表开始爬取。
4.custom_settings:用来存放蜘蛛专属配置的字典,这里的设置会覆盖全局的设置。
5.crawler:由from_crawler()方法设置的和蜘蛛对应的Crawler对象,Crawler对象包含了很多项目组件,利用它我们可以获取项目的配置信息,如调用crawler.settings.get()方法。
6.settings:用来获取爬虫全局设置的变量。
7.start_requests():此方法用于生成初始请求,它返回一个可迭代对象。该方法默认是使用GET请求访问起始URL,如果起始URL需要使用POST请求来访问就必须重写这个方法。
8.parse():当Response没有指定回调函数时,该方法就会被调用,它负责处理Response对象并返回结果,从中提取出需要的数据和后续的请求,该方法需要返回类型为Request或Item的可迭代对象(生成器当前也包含在其中,因此根据实际需要可以用return或yield来产生返回值)。
9.closed():当蜘蛛关闭时,该方法会被调用,通常用来做一些释放资源的善后操作。
感觉这个课件有点虎头蛇尾。。。
7.24
Scrapy分布式实现
1.安装Scrapy-Redis。
2.配置Redis服务器。
3.修改配置文件。
SCHEDULER = 'scrapy_redis.scheduler.Scheduler'
DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
REDIS_HOST = '1.2.3.4'
REDIS_PORT = 6379
REDIS_PASSWORD = '1qaz2wsx'
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue'
SCHEDULER_PERSIST = True(通过持久化支持接续爬取)
SCHEDULER_FLUSH_ON_START = True(每次启动时重新爬取)
Scrapyd分布式部署
1.安装Scrapyd
2.修改配置文件
mkdir /etc/scrapyd
vim /etc/scrapyd/scrapyd.conf
3.安装Scrapyd-Client
将项目打包成Egg文件,通过addversion.json接口部署到Scrapyd上。
7.25
实战无课件
7.26
机器学习的一般步骤:
1.数据收集
2.数据准备
3.数据分析
4.训练算法
5.测试算法
6.应用算法
7.27
Pandas的应用(只有标题无内容)
7.28
NumPy和SciPy的应用(只有标题无内容)
7.29(Matplotlib和数据可视化)
1.安装matplotlibpip3 install matplotlib
2.绘制折线图和散点图
import matplotlib.pyplot as plt
def main():
# 保存x轴数据的列表
x_values = [x for x in range(1, 11)]
# 保存y轴数据的列表
y_values = [x ** 2 for x in range(1, 11)]
# 设置图表的标题以及x和y轴的说明
plt.title('Square Numbers')
plt.xlabel('Value', fontsize=18)
plt.ylabel('Square', fontsize=18)
# 设置刻度标记的文字大小
plt.tick_params(axis='both', labelsize=16)
# 绘制折线图
# plt.plot(x_values, y_values)
# 绘制散点图
plt.axis([0, 12, 0, 120]) # 调整x轴和y轴的坐标范围
plt.plot(x_values, y_values, 'xr')
plt.show()
if __name__ == '__main__':
main()
3.绘制正弦曲线
import matplotlib.pyplot as plt
import numpy as np
def main():
# 指定采样的范围以及样本的数量
x_values = np.linspace(0, 2 * np.pi, 1000)
# 计算每个样本对应的正弦值
y_values = np.sin(x_values)
# 绘制折线图(线条形状为--, 颜色为蓝色)
plt.plot(x_values, y_values, '--b')
# 绘制折线图(线条形状为--, 颜色为红色)
plt.plot(x_values, np.sin(2 * x_values), '--r')
plt.show()
if __name__ == '__main__':
main()
4.在两个坐标系上绘制出两条曲线
import matplotlib.pyplot as plt
import numpy as np
def main():
# 将样本数量减少为50个
x_values = np.linspace(0, 2 * np.pi, 50)
# 设置绘图为2行1列活跃区为1区(第一个图)
plt.subplot(2, 1, 1)
plt.plot(x_values, np.sin(x_values), 'o-b')
# 设置绘图为2行1列活跃区为2区(第二个图)
plt.subplot(2, 1, 2)
plt.plot(x_values, np.sin(2 * x_values), '.-r')
plt.show()
if __name__ == '__main__':
main()
5.绘制直方图
import matplotlib.pyplot as plt
import numpy as np
def main():
# 通过random模块的normal函数产生1000个正态分布的样本
data = np.random.normal(10.0, 5.0, 1000)
# 绘制直方图(直方的数量为10个)
plt.hist(data, 10)
plt.show()
if __name__ == '__main__':
main()
6.使用Pygal绘制矢量图
from random import randint
import pygal
def roll_dice(n=1):
total = 0
for _ in range(n):
total += randint(1, 6)
return total
def main():
results = []
# 将两颗色子摇10000次记录点数
for _ in range(10000):
face = roll_dice(2)
results.append(face)
freqs = []
# 统计2~12点各出现了多少次
for value in range(2, 13):
freq = results.count(value)
freqs.append(freq)
# 绘制柱状图
hist = pygal.Bar()
hist.title = 'Result of rolling two dice'
hist.x_labels = [x for x in range(2, 13)]
hist.add('Frequency', freqs)
# 保存矢量图
hist.render_to_file('result.svg')
if __name__ == '__main__':
main()
7.30
k最近邻分类无课件
7.31
决策树无课件
8.1
贝叶斯分类无课件
8.2
支持向量机无课件
8.3
K-均值聚类无课件
8.4
回归分析无课件
8.5
大数据分析入门无课件
8.6
大数据分析进阶无课件
8.7
Tensorflow入门无课件
8.8
Tensorflow实战无课件
8.9
推荐系统实战无课件
8.10
团队项目开发准备
1.传统的沟通方式无法确定处理的优先级(缺陷管理工具)
2.没有能够用于验证的环境(实施持续交付)
3.用别名目录管理项目分支及重新制作数据库非常困难(实施版本控制)
4.不运行系统就无法察觉问题(实施持续集成,将团队成员的工作成果经常、持续的进行构建和测试)
5.覆盖了其他成员修正的代码(实施版本控制)
6.无法实施代码重构(大量的可重用的测试并实施持续集成)
7.不知道bug的修正日期无法追踪退化(版本控制系统、缺陷管理系统和持续集成之间需要交互,最好能够和自动化部署工具集成到一起来使用)
8.发布过程太复杂(实施持续交付)
9.推荐工具
- 版本控制:git
- 缺陷管理:Redmine
- 持续集成:Jenkins和TravisCI
8.11
1.Docker简介:Docker属于对Linux容器技术的一种封装(利用了Linux的namespace和cgroup技术),它提供了简单易用的容器使用接口,是目前最流行的 Linux 容器解决方案。Docker将应用程序与该程序的依赖打包在一个文件里面,运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。
2.安装Docker:
- 确定操作系统内核版本(CentOS 7要求64位,内核版本3.10+;CentOS 6要求64位,内核版本2.6+),可以通过
uname -r
确定Linux系统内核版本。 - 使用yum安装Docker并启动:
yum -y install docker
systemctl start docker
- 查看Docker的信息和版本
docker version
docker info
- 从Docker的镜像仓库下载名为hello-world的镜像文件
docker pull hello-world
- 查看所有镜像文件
docker images
- 通过镜像文件创建并运行容器
docker container run --name 容器名 境像文件名
- 删除这个容器
docker container rm mycontainer
- 删除刚才镜像文件
docker rmi 镜像文件名
- 将服务器更换为国内镜像,修改 /etc/docker/daemon.js 文件
{
"registry-mirrors": [
"http://hub-mirror.c.163.com",
"https://registry.docker-cn.com"
]
}
3.使用Docker
- 安装Nginx(在Docker中创建一个容器即可)
docker container run -d -p 80:80 --rm --name mynginx nginx
-d表示容器在后台运行(不产生输出到Shell)并显示容器的ID;-p是用来映射容器的端口到宿主机的端口,冒号前面是宿主机的端口,冒号后面是容器内部使用的端口;--rm表示容器停止后自动删除容器,例如执行命令docker container stop mynginx后,容器就不复存在了;--name后面的mynginx是自定义的容器名字;在创建容器的过程中,需要用到nginx的镜像文件,镜像文件的下载是自动完成的,如果没有指定版本号,默认是最新版本(latest)
- 将自己的Web项目(页面)部署到Nginx上,可以使用容器拷贝命令将指定路径下所有的文件和文件夹拷贝到容器的指定目录中
docker container cp /root/web/index.html mynginx:/usr/share/nginx/html
- 重新创建容器(要先停止当前运行的容器)
docker container run -d -p 80:80 --rm --name mynginx --volume $PWD/html:/usr/share/nginx/html nginx
container是可以省略的,也就是说docker container run和docker run是一样的,而docker container cp和docker cp是一样的。此外,命令中的--volume也可以缩写为-v,就如同-d是--detach的缩写,-p是--publish的缩写。$PWD代表宿主系统当前文件夹
- 查看运行中的容器
docker ps
- 启动和停止容器
docker start mynginx
docker stop mynginx
- 查看所有容器
docker container ls -a
- 删除容器(删除正在运行中的容器,需要在rm后加-f选项)
docker rm -f mynginx
3.安装MySQL - 先检查一下有没有MySQL的镜像文件
docker search mysql
- 下载MySQL镜像并指定镜像的版本号
docker pull mysql:5.7
- 查看已经下载的镜像文件
docker images
- 创建并运行MySQL容器
docker run -d -p 3306:3306 --name mysql57 -v $PWD/mysql/conf:/etc/mysql/mysql.cnf.d -v $PWD/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:5.7
4.安装Redis
5.构建镜像(docker commit不推荐,docker build命令和Dockerfile文件)
6.Dockerfile指令
7.容器编排
8.12
MySQL性能优化
1.使用索引
- B-Tree索引
- HASH索引
- R-Tree索引(空间索引)
- Full-text索引(全文索引)
2.SQL优化
- 通过show status了解各种SQL的执行频率
- 定位低效率的SQL语句 - 慢查询日志(
show processlist
) - 通过
explain
了解SQL的执行计划 - 通过show profiles和show profile for query分析SQL
- 优化CRUD操作
- 优化insert语句
- 优化order by语句
- 优化group by语句
- 优化嵌套查询
- 优化or条件
- 优化分页查询
- 使用SQL提示
- USE INDEX
- IGNORE INDEX
- FORCE INDEX
3.配置优化
- 调整max_connections
- 调整back_log
- 调整table_open_cache
- 调整thread_cache_size
调整innodb_lock_wait_timeout
4.架构优化
- 通过拆分提高表的访问效率
- 垂直拆分
- 水平拆分
- 逆范式理论
- 数据表设计的规范程度称之为范式(Normal Form)
- 1NF:列不能再拆分
- 2NF:所有的属性都依赖于主键
- 3NF:所有的属性都直接依赖于主键(消除传递依赖)
- BCNF:消除非平凡多值依赖
- 数据表设计的规范程度称之为范式(Normal Form)
- 使用中间表提高统计查询速度
- 主从复制和读写分离
- 配置MySQL集群
8.13
1.阮一峰老师的《理解RESTful架构》以及《RESTful API设计指南》。
2.rukk文档撰写
8.14
关于Django的一些面试问答(https://github.com/jackfrued/Python-100-Days/blob/master/Day91-100/95.使用Django开发商业项目.md)
8.15
1.软件测试是一种用来促进鉴定软件的正确性、完整性、安全性和品质的过程,也就是在规定的条件下对程序进行操作以发现程序中的错误,衡量软件的品质并对其是否能满足设计要求进行评估的过程
2.测试的方法:
- 黑盒测试:测试应用程序的功能,而不是其内部结构或运作。测试者不需具备应用程序的代码、内部结构和编程语言的专门知识。测试者只需知道什么是系统应该做的事,即当键入一个特定的输入,可得到一定的输出。测试案例是依应用系统应该做的功能,照规范、规格或要求等设计。测试者选择有效输入和无效输入来验证是否正确的输出。此测试方法可适合大部分的软件测试,例如集成测试和系统测试
- 白盒测试:测试应用程序的内部结构或运作,而不是测试应用程序的功能(即黑箱测试)。在白箱测试时,以编程语言的角度来设计测试案例。测试者输入数据验证数据流在程序中的流动路径,并确定适当的输出,类似测试电路中的节点
3.测试的种类
- 单元测试:对软件组成单元进行测试,其目的是检验软件基本组成单位的正确性,测试的对象是软件设计的最小单位 - 函数《Python必会的单元测试框架 - unittest》。
- 集成测试:将程序模块采用适当的集成策略组装起来,对系统的接口及集成后的功能进行正确性检测的测试工作。其主要目的是检查软件单位之间的接口是否正确,集成测试的对象是已经经过单元测试的模块。
- 系统测试:系统测试主要包括功能测试、界面测试、可靠性测试、易用性测试、性能测试。
- 回归测试:为了检测代码修改而引入的错误所进行的测试活动。回归测试是软件维护阶段的重要工作,有研究表明,回归测试带来的耗费占软件生命周期的1/3总费用以上。
4.测试驱动开发
5.Selenium/Robot Framework:Selenium是实现Web应用程序的功能测试以及集成测试自动化的浏览器驱动测试工具群.和使用浏览器的用户相同,Selenium可以在浏览器进行的鼠标操作、在表单中输入文字、验证表单的值等,利用这一点就可以将手动操作变成自动化操作。
8.16
8.17(项目部署上线指南)
1.上线前的检查工作(python manage.py check --deploy)
2.将DEBUG设置为False并配置ALLOWED_HOSTS。
DEBUG = False
ALLOWED_HOSTS = ['*']
3.安全相关的配置
# 保持HTTPS连接的时间
SECURE_HSTS_SECONDS = 3600
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# 自动重定向到安全连接
SECURE_SSL_REDIRECT = True
# 避免浏览器自作聪明推断内容类型
SECURE_CONTENT_TYPE_NOSNIFF = True
# 避免跨站脚本攻击
SECURE_BROWSER_XSS_FILTER = True
# COOKIE只能通过HTTPS进行传输
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# 防止点击劫持攻击手段 - 修改HTTP协议响应头
# 当前网站是不允许使用<iframe>标签进行加载的
X_FRAME_OPTIONS = 'DENY'
4.敏感信息放到环境变量或文件中
SECRET_KEY = os.environ['SECRET_KEY']
DB_USER = os.environ['DB_USER']
DB_PASS = os.environ['DB_PASS']
REDIS_AUTH = os.environ['REDIS_AUTH']
5.注册域名、解析域名以及购买权威机构颁发的证书
6.常用开源软件。
功能 | 开源方案 |
---|---|
版本控制工具 | Git、Mercurial、SVN |
缺陷管理 | Redmine、Mantis |
负载均衡 | Nginx、LVS、HAProxy |
邮件服务 | Postfix、Sendmail |
HTTP服务 | Nginx、Apache |
消息队列 | RabbitMQ、ZeroMQ、Redis |
文件系统 | FastDFS |
基于位置服务(LBS) | MongoDB、Redis |
监控服务 | Nagios、Zabbix |
关系型数据库 | MySQL、PostgreSQL |
非关系型数据库 | MongoDB、Redis、Cassandra |
搜索引擎 | ElasticSearch、Solr |
缓存服务 | Mamcached、Redis |
7.常用云服务
功能 | 可用的云服务 |
---|---|
团队协作工具 | Teambition、钉钉 |
代码托管平台 | Github、Gitee、CODING |
邮件服务 | SendCloud |
云存储(CDN) | 七牛、OSS、LeanCloud、Bmob、又拍云、AWS |
移动端推送 | 极光、友盟、百度 |
即时通信 | 环信、融云 |
短信服务 | 云片、极光、Luosimao、又拍云 |
第三方登录 | 友盟、ShareSDK |
网站监控和统计 | 阿里云监控、监控宝、百度云观测、小鸟云 |
8.18
8.19
全文完。。。
网友评论