文章目录
- datetime
- 获取当前日期和时间
- 获取指定日期和时间
- datetime转化为timestamp
- timestamp转化为datetime
- str转化为datetime
- datetime转化为str
- datetime加减
- collections
- namedtuple
- deque
- defaultdict
- ChainMap
- Counter
- base64
- hashlib
- 摘要算法简介
- MD5摘要算法使用
- 摘要算法的应用
- hmac
- itertools
- count
- cycle
- repeat
- accumulate
- chain
- dropwhile
- takewhile
- groupby
- 计算圆周率的小例子
- contextlib
- closing
- urllib
- Get
- 模拟浏览器去访问
- Post
- 单独的用户登录
- 登录 站长啦 网站(https)
- 添加和查看用户信息
- urllib总结
- XML
- dom
- sax
- ElementTree
- 三种方式的代码运行截图
- HTMLParser
- 实战例子
- 参考网址
datetime
datetime
是Python处理日期和时间的标准库。
获取当前日期和时间
注意下面第一个datetime
是包名,第二个datetime
是类名
# 获取当前日期和时间
from datetime import datetime
now = datetime.now()
print(now)
print(type(now))
获取指定日期和时间
#获取指定日期和时间
from datetime import datetime
# 注意datetime的初始化方法里 年月日时分秒 都是int类型
dt = datetime(2020, 2, 10, 22, 15, 30)
print(dt)
datetime转化为timestamp
在计算机中,时间实际上是用数字表示的。我们把
1970年1月1日 00:00:00 UTC+00:00
时区的时刻称为epoch time
,记为0(1970年以前的时间timestamp为负数),当前时间就是相对于epoch time的秒数,称为timestamp。
仔细理解上面那段话。可以这样认为
timestamp = 0 = 1970-1-1 00:00:00 UTC+0:00
对应北京时间就是
timestamp = 0 = 1970-1-1 08:00:00 UTC+8:00
可以看出timestamp的值与时区是无关的。因为一旦timestamp确定,其UTC时间就确定了,转换到任意时区也是可以完全确定的。这也是为什么这计算机存储的当前时间是以timestamp表示的。
from datetime import datetime
dt = datetime(2020, 2, 10, 22, 15, 30)
print(dt)
# 获取datetime对应的timestamp
print(dt.timestamp())
注意Python的timestamp是一个浮点数。如果有小数位,小数位表示毫秒数。
某些编程语言(如Java和JavaScript)的timestamp使用整数表示毫秒数,这种情况下只需要把timestamp除以1000就得到Python的浮点表示方法。
timestamp转化为datetime
我们知道timestamp是和时区没有关系的,但是datetime是和时区有关的。
# timestamp转化为datetime
ts = 1581344130.0
# 转化成本地时间,即北京时间,UTC+8
local_dt = datetime.fromtimestamp(ts)
# 转化成utc时间,即UTC+0
utc_dt = datetime.utcfromtimestamp(ts)
print(local_dt)
print(utc_dt)
我们可以从打印结果也能看出datetime是和时区有关的,北京时间就比UTC时间+8。
str转化为datetime
用datetime.strptime(cls, date_string, format)
,注意是datetime
类的类方法。
我们常用的日期格式就是下面的格式,更加的详细可以参考Python官网日期格式
# str转化为datetime
dt = datetime.strptime("2020-05-20 20:05:10", "%Y-%m-%d %H:%M:%S")
print(dt)
print(type(dt))
datetime转化为str
采用datetime.strftime(self, fmt)
格式化datetime
# datetime转化为str
s = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(s)
datetime加减
对于日期和时间的加减需要用到datetime
包里的timedelta
类。
其中timedelta
类支持days
、weeks
、hours
、minutes
、seconds
等等
def __init__(self, days: float = ..., seconds: float = ..., microseconds: float = ...,
milliseconds: float = ..., minutes: float = ..., hours: float = ...,
weeks: float = ...)
# datetime加减
from datetime import datetime,timedelta
now = datetime.now()
print("当前时间是: ",now)
print("后退5天的时间是: ",now + timedelta(days=5))
print("前进5天的时间是: ",now - timedelta(days=5))
但是时间加减上并没有前几年或者前几个月,这就需要另一个arrow
模块去实现,不过这不是内建模块,是第三方模块,具体使用可查看Arrow官网。
# 从timestamp获取arrow
a = arrow.get(1581344130.0)
# 注意这里的format方法和strftime方法的参数格式是不同的
print(a.format("YYYY-MM-DD HH:mm:ss"))
# 从str获取arrow
a = arrow.get("2020-02-10 22:15:30", "YYYY-MM-DD HH:mm:ss")
print(a.format("YYYY-MM-DD HH:mm:ss"))
# 获取前2年 后3月 的时间
print("前2年 后3月 的时间: ",a.shift(years=-2, months=3).format("YYYY-MM-DD HH:mm:ss"))
collections
collections
是Python内建的一个集合模块,提供了许多有用的集合类。
namedtuple
我们知道tuple
里的元素是不可变的,但是访问其中元素时是和list
一样通过索引下标
访问。
例如,我们定义一个平面系坐标的点(x,y),就可以用namedtuple
来定义,然后通过名字来访问。如下所示
# namedtuple 通过名字访问tuple元素
from collections import namedtuple
import math
Point = namedtuple("Point",["x", "y"])
p = Point(3,4)
print("点p的横坐标为{}, 纵坐标为{}, 其到原点长度是{}".format(p.x, p.y, math.sqrt(math.pow(p.x,2)+math.pow(p.y,2))))
# 此处Point已经是一个类型
print("p是Point? ", isinstance(p, Point))
print("p是tuple? ",isinstance(p, tuple))
deque
使用list
存储数据时,按索引访问元素很快,但是插入和删除元素就很慢了,因为list是线性存储,数据量大的时候,插入和删除效率很低
deque是为了高效实现插入和删除操作的双向列表
,适合用于队列和栈
。
from collections import deque
q = deque(['a', 'b', 'c'])
q.append('d')
q.appendleft('1')
print(q)
print("弹出右边的元素: ", q.pop())
print("弹出左边的元素: ", q.popleft())
print(q)
defaultdict
使用dict
时,如果引用的Key
不存在,就会抛出KeyError
。如果希望key不存在时,返回一个默认值
,就可以用defaultdict
注意defaultdict
里面的参数是一个函数,该函数无参,但是有返回值即默认值。
# defaultdict 当key不存在时返回默认值
from collections import defaultdict
d = defaultdict(lambda : 0)
d['patrick']=100
print('patrick的分数是: ', d['patrick'])
print('marry的分数是(默认值): ', d['marry'])
ChainMap
ChainMap
可以把一组dict
串起来并组成一个逻辑上的dict
。ChainMap本身也是一个dict,但是查找的时候,会按照顺序在内部的dict依次查找
。
这个在向应用程序传入参数时非常适用。向应用程序传入既可以通过命令行传入,也可以通过环境变量,程序也有默认参数。但是我们可以使用ChainMap
来按序查找参数的值,即可以先从命令行查找,然后从环境变量查找,最后查默认值。
这个可以结合Hadoop的配置来理解,Hadoop优先支持命令行,然后支持配置文件,再支持默认值。
# ChainMap
from collections import ChainMap
import os
import argparse
# 构造缺省参数
defaults = {"dfs_replication": 3, "mapreduce_job_reduces": 0}
# 构造命令行参数
parser = argparse.ArgumentParser()
parser.add_argument("-dr", "--dfs_replication")
parser.add_argument("-mrn", "--mapreduce_job_reduces")
namespace = parser.parse_args()
# 获取v不为空的键值对
command_line = {k:v for k,v in vars(namespace).items() if v}
combined_maps = ChainMap(command_line, os.environ, defaults)
print("dfs_replication=",combined_maps["dfs_replication"])
print("mapreduce_job_reduces=",combined_maps["mapreduce_job_reduces"])
下面分别展示了三种情况下的参数的值。
注意:这里之所以把.
换成了_
是因为环境变量不支持.
而且需要使用export
将变量使得后面的子进程可见,否则os.environ
会获取不到设置的环境变量值。
这里由于使用了 argparse
模块,故可以直接输入-h
来查看其帮助信息。当然这个帮助信息是自动生成的。关于 argparse
模块后面会有专门的章节去讲,这也是Python内建模块,详细可以直接参考官网argparse模块。
Counter
Counter
是一个简单的计数器。
能非常方便的计数和统计前N的字符及其出现次数。
from collections import Counter
d = Counter()
for c in "programming":
d[c] = d[c]+1
print(d)
print("统计前2的字符是", d.most_common(2))
# 可以传入iterable
print(Counter("programming"))
# 可以通过k=v传入
print(Counter(a=3, b=2))
# 可以通过dict传入
print(Counter({"a":3, "b":2}))
base64
Base64
是一种用64个字符来表示任意二进制数据的方法。
由于二进制文件如图片、exe文件等包含很多无法显示和打印的字符,所以,如果要让记事本这样的文本处理软件能处理二进制数据,就需要一个二进制到字符串的转换方法。Base64是一种最常见的二进制编码方法。
Base64的原理很简单,首先,准备一个包含64个字符的数组:
['A', 'B', 'C', ... 'a', 'b', 'c', ... '0', '1', ... '+', '/']
然后,对二进制数据进行处理,每3个字节一组,一共是3x8=24bit,划为4组,每组正好6个bit:
这样我们得到4个数字作为索引,然后查表,获得相应的4个字符,就是编码后的字符串。
所以,Base64编码会把3字节的二进制数据编码为4字节的文本数据,长度增加33%,好处是编码后的文本数据可以在邮件正文、网页等直接显示。
如果要编码的二进制数据不是3的倍数,最后会剩下1个或2个字节怎么办?Base64用\x00字节在末尾补足后,再在编码的末尾加上1个或2个=号,表示补了多少字节,解码的时候,会自动去掉。
所以Base64编码后的字符串一定是4的倍数,如果不是4的倍数就需要再后面补相应个数的=号。
这里需要说明下=
号只会出现在最后面作为补足位数,前提是表里本身就不包含=
。
如下所示,需要注意的是 b64encode 和 b64decode 方法的输入参数和返回参数都是bytes类型的字符串
# -*- coding:UTF-8 -*-
import base64
# b64encode 和 b64decode 方法的输入参数和返回参数都是bytes类型的字符串
# 编码后的是bytes类型字符串
e_str = base64.b64encode("大数据平台yarn".encode())
print(e_str)
# <class 'bytes'>
print(type(e_str))
s = base64.b64decode(e_str)
print(s.decode())
# 该方法是用于处理那些尾部已经去掉等号的bytes类型字符串
def safe_base64_decode(s):
s = s.decode()
left = len(s) % 4
list_str = []
list_str.append(s)
for i in range(left):
list_str.append("=")
return base64.b64decode("".join(list_str).encode()).decode()
assert '大数据平台yarn' == safe_base64_decode(b'5aSn5pWw5o2u5bmz5Y+weWFybg=='), "带等号解码失效"
assert '大数据平台yarn' == safe_base64_decode(b'5aSn5pWw5o2u5bmz5Y+weWFybg'), "不带等号解码失效"
assert '大数据平台' == safe_base64_decode(b'5aSn5pWw5o2u5bmz5Y+w'), "带等号解码失效"
print('ok')
Base64是一种通过查表的编码方法,不能用于加密,即使使用自定义的编码表也不行(一般也不需要自定义表)。
Base64适用于小段内容的编码,比如数字证书签名、Cookie的内容等
hashlib
摘要算法简介
摘要算法
又称为哈希算法
、散列算法
。它通过一个函数把任意长度的数据转换为一个固定的数据串(如16位固定长度)。
摘要算法的目的是为了发现原始数据是否被人篡改过,如Apache下的包有的也会附带着摘要,来判断下载的包是否是完整的未经篡改的包,只是平时我们不去校验摘要而已。
摘要算法之所以能够指出原数据是否被人篡改过,主要在于摘要函数是一个单向函数,计算摘要很容易,但是通过摘要反推出原数据就很困难,任意一个bit的修改都会导致计算出摘要完全不同。
Python的hashlib
提供了常见的摘要算法,如MD5,SHA1等等。
MD5摘要算法使用
下面是常用的MD5
摘要算法的使用方法。update()
:传入原始数据(注意必须是bytes
类型),可以多次调用digest()
:计算原始数据的摘要,返回的类型是byteshexdigest()
:计算原始数据的摘要,返回的类型是32位固定长度的字符串
import hashlib
# 获取md5算法
md5 = hashlib.md5()
# 将Bytes类型的字符串传入update方法里, 可以多次传入
md5.update('how to use md5 in python hashlib?'.encode('utf-8'))
md5.update('just use update & hexdigest'.encode('utf-8'))
# 计算到目前位置通过update传入到该md5里所有数据的摘要, 返回bytes类型的字符串
print(md5.digest())
# 返回32位固定长度的16进制字符串, 其类型是str
print(md5.hexdigest())
python的hashlib
模块还支持如下算法,但是使用方法都是和md5
类似。
越安全的摘要算法长度越长,耗时越高。
摘要算法的应用
最常见的就是在数据库中对用户的密码取md5摘要并代替明文密码存到数据库中,这样可以防止用户密码随意暴露给运维人员。
但是这样也不一定安全。如果用户设置如123456
、password
等简单的密码,黑客完全可以事先算出常用密码的摘要并构造出一个反推表即通过摘要推出简单的密码。
我们可以在程序设计上对简单密码进行加强保护,俗称加盐
即Salt
。即对原始密码添加一个复杂的字符串然后再进行摘要计算。只要Salt
没有暴露,那么很难通过摘要计算出明文密码。
如果有两个用户使用同样的密码,那么保存在数据库中的摘要是一样的,如何让相同口令的用户存储不同的摘要呢?可以把用户名作为Salt
的一部分,从而实现相同口令的用户存储不同的摘要。
hmac
为了防止黑客通过彩虹表根据哈希值反推出明文口令,根据上面内容可以采用加盐
的方式使得相同的输入得到不同的哈希值,大大增加黑客的破解难度。
其实加盐
这种方式就是Hmac
算法:Keyed-Hashing for Message Authentication
。它通过一个标准算法,在计算哈希的过程中,把key混入计算过程中。采用Hmac替代我们自己的salt算法,可以使程序算法更标准化,也更安全。
Python自带的hmac
模块实现了标准的Hmac算法。
下面是Hmac算法的例子,和上面的MD5使用方法一样。
import hmac
key = b'secret key'
message = "how to use md5 in python hashlib?".encode()
h = hmac.new(key, message, 'md5')
# 可以通过upadte方法传入数据
h.update('just use update & hexdigest'.encode())
print(h.hexdigest())
itertools
itertools
模块提供了很多用来创建和使用迭代对象的函数。
下面图展示了itertools
模块的源码简介
count
count(start=0, step=1) --> start, start+step, start+2*step, ...
会创建一个无限迭代器。
如下所示,打印100以内的自然数,由于是无限迭代器,所以测试代码里设置了退出条件。
import itertools
# 打印100以内的自然数
num = itertools.count(1)
for n in num:
if n <= 100:
print(n)
else:
break
cycle
cycle(p) --> p0, p1, ... plast, p0, p1, ...
会创建一个无限迭代器。
下面代码展示了cycle
的使用方法,字符串是可迭代的。为了使测试代码能退出故使用了enumerate
计算迭代次数。
iter_c = itertools.cycle("hadoop")
for i,c in enumerate(iter_c):
if i< 10:
# 此处为了让打印看的更清除,就将sep和end设置成空字符串
print(c, sep="", end="")
else:
break
repeat
repeat(elem [,n]) --> elem, elem, elem, ... endlessly or up to n times
负责将一个元素无限重复下去,也可以指定参数来指定重复次数。
如下所示,将字符串hadoop
重复3次。
iter_s = itertools.repeat("hadoop", 3)
for s in iter_s:
print(s)
accumulate
accumulate(p[, func]) --> p0, p0+p1, p0+p1+p2
会创建一个不断累积的无限迭代器,其中累积函数默认是求和。
如下面代码就展示了求自然数前N项和的函数。
def sum_n(n):
iter_num = itertools.count(1)
iter_sum = itertools.accumulate(iter_num)
for i, s in enumerate(iter_sum):
if i == 10:
break
print("自然数前{}项和为{}".format((i+1), s))
if __name__ == "__main__":
sum_n(10)
pass
chain
chain(p, q, ...) --> p0, p1, ... plast, q0, q1, ...
可以将已有的迭代器串起来形成更大的迭代器。
如下面所示,将两个字符串迭代器串起来形成一个大的迭代器。
iter_chain = itertools.chain("hadoop", "spark")
for c in iter_chain:
# 此处为了让打印看的更清除,就将sep和end设置成空字符串
print(c, sep="", end="")
dropwhile
dropwhile(pred, seq) --> seq[n], seq[n+1], starting when pred fails
也会创建一个子迭代器。
如下面例子展示了从自然数中的11开始前十个自然数。
def print_ten(iter_element):
for i, element in enumerate(iter_element):
if i == 10:
break
print(element)
iter_n = itertools.dropwhile(lambda x:x<11, itertools.count(1))
print_ten(iter_n)
takewhile
takewhile(pred, seq) --> seq[0], seq[1], until pred fails
会创建一个迭代器。
如下面代码展示了使用takewhile
取奇数项前十个元素
n=10
odd = itertools.count(1, step=2)
ten_odd = itertools.takewhile(lambda v: (v+1)/2 <= n, odd)
print(list(ten_odd))
groupby
groupby()
把迭代器中相邻的重复元素挑出来放在一起。可以类比MapReduce任务的Reduce阶段,不过又不同于Reduce,因为这个迭代器中相同的元素并不是全部都是相邻的。
所以请特别注意是相邻的重复元素。
下面展示了groupby()
使用的例子及其结果
iter_groups = itertools.groupby('AAAABBCCCCDDD')
for k,sub_iter in iter_groups:
print("*******************************")
print("k={}".format(k))
print(list(sub_iter))
实际上挑选规则是通过函数完成的,只要作用于函数的两个元素返回的值相等,这两个元素就被认为是在一组的,而函数返回值作为组的key。如果我们要忽略大小写分组,就可以让元素’A’和’a’都返回相同的key。
下面的例子就是让相邻的大小写字母归为同一组。
iter_groups = itertools.groupby('AAaaABBCccCDdd', lambda v: v.upper())
for k,sub_iter in iter_groups:
print("*******************************")
print("k={}".format(k))
print(list(sub_iter))
计算圆周率的小例子
根据提供的四个步骤去计算圆周率,如果打印出ok则证明算法是正确的。
def pi(n):
""" 计算pi的值
step 1: 创建一个奇数序列: 1, 3, 5, 7, 9, ...
step 2: 取该序列的前N项: 1, 3, 5, 7, 9, ..., 2*N-1.
step 3: 添加正负符号并用4除: 4/1, -4/3, 4/5, -4/7, 4/9, ...
step 4: 求和:
"""
# step 1
odd = itertools.count(1, step=2)
# step 2
n_odd = itertools.takewhile(lambda v: (v + 1) / 2 <= n, odd)
# step 3 注意这里采用生成式的方式生成迭代对象
iter_n = ( -4/i if (i+1)/2%2 == 0 else 4/i for i in n_odd)
# step 4
from functools import reduce
return reduce(lambda x,y:x+y, iter_n)
def check_pi():
print(pi(10))
print(pi(100))
print(pi(1000))
print(pi(10000))
assert 3.04 < pi(10) < 3.05
assert 3.13 < pi(100) < 3.14
assert 3.140 < pi(1000) < 3.141
assert 3.1414 < pi(10000) < 3.1415
print('ok')
if __name__ == "__main__":
check_pi()
pass
contextlib
在Python里我们是通过with
语句来自动关闭文件资源的,不需要写try ... finally ...
这种繁琐的语句。
并不是只有open()
函数返回的文件描述符对象才能使用with
语句。实际上,任何对象,只要正确实现了上下文管理,就可以使用with
语句关闭相应资源。
实现上下文管理是通过__enter__
和__exit__
这两个方法实现的。
下面的代码就展示了如何让自定义类可以使用with
语句自动关闭资源。
class Query(object):
def __init__(self, name):
self.name=name
def __enter__(self):
print("Begin")
# 注意必须返回当前对象实例 否则使用with语句会报错
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type:
print("Error")
else:
print("End")
def query(self):
print("Query info about {}".format(self.name))
with Query("patrick") as q:
q.query()
实际上每个类都要这么去实现两个方法也挺繁琐的。
我们可以使用contextlib
模块提供的contextmanager
去简化代码。
如下面代码所示。@contextmanager
这个装饰器接受一个generator
,用yield
语句把with ... as var
把变量输出出去,然后,with
语句就可以正常地工作了。
class Query(object):
def __init__(self, name):
self.name=name
def query(self):
print("Query info about {}".format(self.name))
from contextlib import contextmanager
@contextmanager
def create_query(name):
print("Begin")
q = Query(name)
yield q
print("End")
with create_query("patrick") as q:
q.query()
很多时候我们希望在某些代码块前后自动执行特定代码,可以使用@contextmanager
来实现。
如当向数据库执行一条查询语句时,可以按照下面的方式去计算查询时间并打印出来。
from contextlib import contextmanager
import time
@contextmanager
def count_time(action):
print("Begin")
start = time.perf_counter()
yield
end = time.perf_counter()
print("{}共耗时{:.0f}s".format(action, (end-start)))
print("End")
with count_time("select"):
import random
# 向数据库执行一条查询语句
time.sleep(random.randint(3,7))
closing
contextlib
中还包含一个closing
对象,这个对象就是一个上下文管理器,它的__exit__
函数仅仅调用传入参数的close
。其源码如下。
所以closeing
上下文管理器仅使用于具有close()
方法的资源对象。如我们通过urllib.urlopen
打开一个网页,urlopen
返回的对象有close
方法,所以我们就可以使用closing
上下文管理器。
from contextlib import closing
from urllib.request import urlopen
with closing(urlopen("https://www.baidu.com/")) as resp:
print(type(resp))
for line in resp:
print(line)
urllib
urllib
模块提供了一系列操作url的功能。
Get
urllib
的request
模块可以非常方便地抓取URL内容,也就是发送一个GET请求到指定的页面,然后返回HTTP的响应。
urlopen()
函数返回的对象类型是http.client.HTTPResponse
。假设下面的resp
就表示HTTPResponse
对象。
resp.info()
和 resp.getheaders()
返回HTTPResponse
的header
信息,只不过后者返回的是列表(如下面源码所示)。可以通过resp.info().get('Content-Type')
来获取具体的header
值,后者的话只能遍历获取了效率没有前者高。
resp.geturl()
获取页面真实的url, 通过和原有的url进行比对可发现是否产生了重定向。
resp.getcode()
获取响应的返回码, 其实就是返回resp.status
。
resp.read()
获取响应的返回内容。
下面代码展示了通过urlopen()
函数来抓取三个网页内容。
其中在抓取https://www.qq.com/
时发现该网页的响应header
里有Content-Encoding: gzip
,表示该网页是通过gzip压缩然后返回给客户端的,客户端需要进行解压缩,所以下面的代码就用了zlib
模块去解压缩。
由于resp.read()
返回的类型是bytes
,所以转换成中文需要解码。一方面可以参考响应header
里的Content-Type
看其具体是什么编码,一方面可以参考第三方库chardet
来检查返回的内容具体是什么编码。
在发现网页编码类型是gb2312
时,直接用gbk
去解码即可,如果用gb2312
反而会报错,具体可看下面代码。
from urllib.request import urlopen
from contextlib import closing
# url = "https://lol.qq.com/" # gb2312编码
url = "https://www.qq.com/" # gb2312编码 并采用了Content-Encoding: gzip 通过压缩网页内容来减少网络传输数据量, 当然客户端就需要解压缩
# url = "" # utf-8编码
with closing(urlopen(url)) as resp:
# 属于http.client.HTTPResponse
print(type(resp))
# 获取返回的header信息, 可以通过resp.info() 也可以通过resp.getheaders()
print(resp.info())
print(resp.getheaders())
# 获取页面真实的url, 通过和原有的url进行比对可发现是否产生了重定向
# 其实就是返回resp.url
print(resp.geturl())
# 获取响应的返回码 其实就是返回resp.status
print(resp.getcode())
# 获取响应返回的内容
data = resp.read()
# 获取其'Content-Type'
print(resp.info().get('Content-Type'))
gzip_val = resp.info().get("Content-Encoding")
if gzip_val:
print("Content-Encoding: ", gzip_val)
# 解压缩
import zlib
data = zlib.decompress(data, 16+zlib.MAX_WBITS)
# 引用第三方包检查其类型 只作为参考, 也可以使用上面提到的'Content-Type'
import chardet
detect_res = chardet.detect(data)
print(detect_res)
if detect_res.get("encoding").lower().find("utf") != -1:
print(data.decode())
else:
# 例如 https://lol.qq.com/ 网页的编码就是gb2312 但是解码的时候直接用gbk解码就好
print(data.decode("gbk"))
模拟浏览器去访问
如果我们要想模拟浏览器发送GET请求,就需要使用Request
对象,通过往Request
对象添加相应的header
信息,我们就可以把请求伪装成浏览器。例如,模拟iPhone 6去请求豆瓣首页
首先通过request.Request(url)
获取Request
对象,然后通过req.add_header()
添加相应的header
信息,再使用urlopen(req)
请求网页,注意此时urlopen()
函数的参数是Request
对象。
下面只展示了部分代码,其余内容和上面的代码保持一致。
其中user-agent
的值可以通过在浏览器上按F12
查看(如下图所示)。
from urllib.request import urlopen
from urllib import request
from contextlib import closing
# url = "https://lol.qq.com/" # gb2312编码
# url = "https://www.qq.com/" # gb2312编码 并采用了Content-Encoding: gzip 通过压缩网页内容来减少网络传输数据量, 当然客户端就需要解压缩
# url = "" # utf-8编码
url = "https://www.douban.com/"
req = request.Request(url)
# req.add_header("user-agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36")
# 模拟手机发送请求
req.add_header("user-agent", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Mobile Safari/537.36")
with closing(urlopen(req)) as resp:
# 获取返回的header信息, 可以通过resp.info() 也可以通过resp.getheaders()
print(resp.info())
print(resp.getheaders())
Post
可以采用post的方式去提交请求。譬如登录。
下面的请求中查看所有用户信息和添加用户信息之前都需要先登录。那么这就涉及到一个保存cookie的问题。
下面的请求是用SpringBoot
写的一个简单的web应用。传递参数都是用的json
,所以在发出请求时都需要在请求的header
信息里添加content-type: application/json
url | 说明 |
/login | 登录 |
/user/list | 查看所有用户信息 |
/user/add | 添加用户信息 |
单独的用户登录
由于传递的是json信息,需要使用json.dumps(login_d).encode("utf8")
将json信息转化成字符串并编码成bytes
类型,这样才能传递给urlopen()
函数的data参数。
from urllib.request import urlopen
from urllib import request
from contextlib import closing
import json
url = "http://localhost:8080/"
req = request.Request(url+"login")
# 登录信息
login_d = {"username": 'admin', "password": 'admin'}
# 将个人信息转化成字符串后并编码转化成bytes类型
post_data = json.dumps(login_d).encode("utf8")
# 由于后台服务支持的是 application/json 而不是application/x-www-form-urlencoded 所以请求对象这里必须显示设置
req.add_header("content-type", "application/json")
with closing(urlopen(req, data=post_data)) as resp:
# 获取响应返回的内容
data = resp.read()
# 后台返回的是json字符串 通过json.loads转化成json对象
res = json.loads(data)
print(res)
print(res.get("msg"))
print(res.get("code"))
登录 站长啦 网站(https)
import ssl
# 如果网站的SSL证书是经过CA认证,就需要单独处理SSL证书,让程序忽略SSL证书验证错误,即可正常访问
context = ssl._create_unverified_context() # 忽略安全
res=urllib.parse.urlencode({"wd": "中文"})
print(res)
wd = urllib.parse.unquote(res)
print(wd)
url = "https://top.cnzzla.com/member/?mod=login"
# 采用 urllib.parse.urlencode去编码
# 其中data如果有值就表明是用post请求, data参数必须转化为bytes类型
resp = urlopen(url, data=bytes(urllib.parse.urlencode({"email": "123@qq.com", "pass": "345", "action": "login"}) , encoding="UTF-8")
,context=context)
print(resp.read().decode("UTF-8"))
添加和查看用户信息
添加用户前,需要登录,如上面所示需要考虑cookie的保存问题。
需要使用下面的代码保存cookie
信息,并且不能需要使用opener
对象去请求,而不是像以前一样用urlopen()
函数。
from urllib import request
from http import cookiejar
from contextlib import closing
# 利用cookie保存登录信息
cookie = cookiejar.CookieJar()
handler = request.HTTPCookieProcessor(cookie)
# 后面用opener去请求而不是用urlopen() 这样就能访问cookie里的登录信息
opener = request.build_opener(handler)
下面是完整的代码
# -*- coding:UTF-8 -*-
from urllib import request
from http import cookiejar
from contextlib import closing
import json
server_url = "http://localhost:8080/"
# 利用cookie保存登录信息
cookie = cookiejar.CookieJar()
handler = request.HTTPCookieProcessor(cookie)
# 后面用opener去请求而不是用urlopen() 这样就能访问cookie里的登录信息
opener = request.build_opener(handler)
def login():
req = request.Request(server_url + "login")
# 登录信息
login_d = {"username": 'admin', "password": 'admin'}
# 将个人信息转化成字符串后并编码转化成bytes类型
post_data = json.dumps(login_d).encode("utf8")
# 由于后台服务支持的是 application/json 而不是application/x-www-form-urlencoded 所以请求对象这里必须显示设置
req.add_header("content-type", "application/json")
# with closing(urlopen(req, data=post_data)) as resp:
with closing(opener.open(req, data=post_data)) as resp:
# 获取响应返回的内容
data = resp.read()
# 后台返回的是json字符串 通过json.loads转化成json对象
res = json.loads(data)
print(res)
def user_list():
req = request.Request(server_url + "user/list")
with closing(opener.open(req)) as resp:
# 获取响应返回的内容
data = resp.read()
# 后台返回的是json字符串 通过json.loads转化成json对象
res = json.loads(data)
# 采用pprint模块打印的更加美化
import pprint
pprint.pprint(res)
def user_add(user):
req = request.Request(server_url + "user/add")
req.add_header("content-type", "application/json")
post_data = json.dumps(user).encode()
with closing(opener.open(req, data=post_data)) as resp:
# 获取响应返回的内容
data = resp.read()
# 后台返回的是json字符串 通过json.loads转化成json对象
res = json.loads(data)
print(res)
if __name__ == "__main__":
login()
user_data = {"username": "鹿丸", "password": "admin", "age": 20, "sex": "男", "money": 500.25, "school": "北京大学"}
user_add(user_data)
user_data = {"username": "丁次", "password": "admin", "age": 20, "sex": "男", "money": 200.25, "school": "火影大学"}
user_add(user_data)
user_data = {"username": "井野", "password": "admin", "age": 20, "sex": "女", "money": 100.25, "school": "中忍大学"}
user_add(user_data)
user_list()
pass
urllib总结
418 响应码表示是反爬
Cookie 是由服务端生成后通过在响应头里设置set-cookie发送给客户端,Cookie总是保存在客户端。
下次客户端访问服务器时就在请求头里设置cookie一并发送给服务端,然后服务端读取cookie信息。
URL编码也称为 百分号编码 percent-encoding。
编码规则十分简单: %加上两位的字符(代表一个字节的十六进制)
URL编码就是将每个非ASCII码的字符替换为%XX形式, RFC建议使用UTF8编码
urllib.requests
用于打开urlurllib.error
包含urllib可能遇到的异常urllib.parse
解析url (对应URL的编码和解码)urllib.robotparser
用于解析robots.txt文件
urllib模块的使用
# get请求
urllib.requests.urlopen(url)
# post请求 注意data是必须是要编码后的bytes类型
urllib.requests.urlopen(url,data=data)
# 构建请求对象应对普通的反爬 添加header信息
req=urllib.requests.Request(url,headers=headers)
urllib.requests.urlopen(req)
# 高匿IP代理应对反爬
proxy_handler = urllib.request.ProxyHandler({"http": "120.194.55.139:6969"})
opener = urllib.request.build_opener(proxy_handler)
print(opener.open(req).read().decode("utf-8"))
# 保存cookie信息
from http import cookiejar
cookie = cookiejar.MozillaCookieJar()
processor = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(processor)
print(opener.open(req).read().decode("utf-8"))
XML
python有三种方式去解析xml文件。个人倾向于第三种方式。
- 使用
dom
去解析。缺点是需要将整个xml文件都读入内存,内存占用高,比较慢。 - 使用
sax
去解析。sax
是采用事件驱动模型,边读入内存边解析,优点是占用内存小,解析快,缺点是需要自己写对应事件的回调函数。 - 使用
ElementTree
去解析。ElementTree 相对于 DOM 来说拥有更好的性能,与 SAX 性能差不多,API 使用也很方便。
准备xml文件如下。
<?xml version="1.0" encoding="utf-8"?>
<list>
<student id="stu1" name="stu1_name">
<id>1001</id>
<name>张三</name>
<age>22</age>
<gender>男</gender>
</student>
<student id="stu2" name="stu2_name">
<id>1002</id>
<name>李四</name>
<age>21</age>
<gender>女</gender>
</student>
</list>
dom
from xml.dom.minidom import parse
xml_path = "d:/test.xml"
def func_dom():
"""
使用dom解析xml文件, 由于dom会将整个xml文件读入内存,占用内存高,解析会比较慢
"""
# 将文件读取成一个dom对象
dom = parse(xml_path)
# 获取文档元素对象 这里获取的是<list>
root = dom.documentElement
# 类型是 xml.dom.minidom.Element
print(type(root))
# 根据tag=student获取所有的student
stus = root.getElementsByTagName("student")
for stu in stus:
# 获取属性值
attr_id = stu.getAttribute("id")
attr_name = stu.getAttribute("name")
# 获取节点值
id = stu.getElementsByTagName("id")[0].childNodes[0].data
name = stu.getElementsByTagName("name")[0].childNodes[0].data
age = stu.getElementsByTagName("age")[0].childNodes[0].data
gender = stu.getElementsByTagName("gender")[0].childNodes[0].data
print("attr_id={}\tattr_name={}\tid={}\tname={}\tage={}\tgender={}".format(
attr_id, attr_name, id, name, age, gender
))
sax
使用sax
最麻烦的就是需要自己写回调函数。需要了解xml.sax.handler.ContentHandler
类中几个事件的调用时机。
characters(content)
方法
从行开始,遇到标签之前,存在字符,content 的值为这些字符串。
从一个标签,遇到下一个标签之前, 存在字符,content 的值为这些字符串。
从一个标签,遇到行结束符之前,存在字符,content 的值为这些字符串。
标签可以是开始标签,也可以是结束标签。- startDocument() 方法
文档启动的时候调用。 - endDocument() 方法
解析器到达文档结尾时调用。 startElement(name, attrs)
方法
遇到XML开始标签时调用,name是标签的名字,attrs是标签的属性值字典。endElement(name)
方法
遇到XML结束标签时调用。
from xml.sax import ContentHandler
import xml.sax as sax
xml_path = "d:/test.xml"
class StudentHandler(ContentHandler):
def __init__(self):
# 存放所有的学生
self.stus = []
def startElement(self, name, attrs):
# 记录当前的element
self.CurrentData = name
if name == "student":
stu = {"attr_id": attrs["id"], "attr_name": attrs["name"]}
self.stus.append(stu)
# 记录当前学生
self.CurrentStu = stu
def endElement(self, name):
if name in ("id", "name", "age", "gender"):
# 清空CurrentData 这是由于characters的调用时机会有三次
# 所以当遇到介绍元素时就可以清空CurrentData保证属性值不会被覆盖
self.CurrentData = ""
pass
def characters(self, content):
if self.CurrentData in ("id", "name", "age", "gender"):
self.CurrentStu[self.CurrentData]=content
pass
def func_sax():
"""
SAX 用事件驱动模型,通过在解析XML的过程中触发一个个的事件并调用用户定义的回调函数来处理XML文件。
边读边解析,优点是占用内存小,缺点是需要自己写回调函数
"""
# 创建一个SAX parser
sax_parser = sax.make_parser()
# 关闭命名空间
sax_parser.setFeature(sax.handler.feature_namespaces, 0)
# 重写Handler
stu_handler = StudentHandler()
sax_parser.setContentHandler(stu_handler)
# 解析xml文件
sax_parser.parse(xml_path)
import pprint
pprint.pprint(stu_handler.stus)
ElementTree
个人比较推荐这种方式去解析xml,主要是api比较友好。
def func_element_tree():
"""
使用ElementTree解析xml
"""
# 解析xml文件为ElementTree对象
tree = ET.parse(xml_path)
# 获取根元素
root = tree.getroot()
# xml.etree.ElementTree.Element
# print(type(root))
for stu in root:
attrs = stu.attrib
stu_d = {"attr_id": attrs["id"], "attr_name": attrs["name"]}
stu_d["id"]=stu.findtext("id")
stu_d["name"]=stu.findtext("name")
stu_d["age"]=stu.findtext("age")
stu_d["gender"]=stu.findtext("gender")
print(stu_d)
三种方式的代码运行截图
将上面三种方式的代码合并在一起后,运行结果截图如下。
HTMLParser
python提供了html.parser.HTMLParser
类去解析HTML。需要注意的是该类也是事件驱动型,和用SAX
去解析xml文件类似。
下面是该类常用的方法:
- HTMLParser.
feed
(data):接收一个字符串类型的HTML内容,并进行解析。 - HTMLParser.
handle_starttag
(tag, attrs):对开始标签的处理方法。例如<div id="main">
,参数tag指的是div,attrs指的是一个(name,Value)的列表,即列表里面装的数据是元组。 - HTMLParser.
handle_endtag
(tag):对结束标签的处理方法。例如</div>
,参数tag指的是div。 - HTMLParser.
handle_startendtag
(tag, attrs):识别没有结束标签的HTML标签,例如<img />
等。 - HTMLParser.
handle_data
(data):对标签之间的数据的处理方法。<tag>test</tag>
,data指的是“test”。
实战例子
我们获取csdn博客首页右下角的企业博客信息, 包括企业博客的 名字、原创数、粉丝数、获赞数。如下图。
思路:首先通过上面的urlopen()
函数去抓取该网页内容,然后通过html.parser.HTMLParser
类去解析HTML获取网页上列出的企业博客信息。个人觉得最重要的是需要仔细观看网页内容,然后根据需求定位到所需要的元素,即需要一定的HTML知识。因为html.parser.HTMLParser
类就是在扫描一个个标签的时候触发的事件(也就是你写的回调函数)。下面贴出信息所在的html部分内容。
下面的代码我在上面的思路基础上又写了一个稍微没那么复杂的代码,即通过re
模块去匹配到所需要的html内容,这样就会大大地较少解析的内容,同时代码上看起来就会简洁一些。
此处就不细讲代码具体的实现逻辑了。代码关键处有注释。
# -*- coding:UTF-8 -*-
from contextlib import closing
from urllib.request import urlopen
from htm
def get_page(url):
data = ""
with closing(urlopen(url)) as resp:
# 获取响应返回的内容
data = resp.read()
return data.decode()
class CsdnHtmlParser(HTMLParser):
def __init__(self):
# 必须对父类进行初始化 要不然运行会报错
HTMLParser.__init__(self)
# 记录一些信息以便后面事件驱动时使用
self.enterprises = []
self.is_tick = False
self.is_tick_data = False
self.CurrentEnterprise = None
self.name_ok = False
def handle_starttag(self, tag, attrs):
if tag == "div" and (("class", "enterprise_r") in attrs):
enterprise = {"msg": "ok"}
self.CurrentEnterprise = enterprise
self.enterprises.append(enterprise)
elif tag == "a" and (("target", "_blank") in attrs):
if self.CurrentEnterprise:
href = list(filter(lambda x: x[0] == "href", attrs))[0][1]
self.CurrentEnterprise["地址"] = href
self.name_ok = True
elif tag == "span" and (("class", "name") in attrs):
if self.CurrentEnterprise:
self.is_tick = True
self.attr_name = None
elif tag == "span" and (("class", "number") in attrs):
if self.CurrentEnterprise and self.attr_name:
self.is_tick_data = True
pass
def handle_startendtag(self, tag, attrs):
# print("tag: ", tag)
pass
def handle_endtag(self, tag):
if tag == "div" and self.CurrentEnterprise:
self.CurrentEnterprise = None
pass
def handle_data(self, data):
if self.CurrentEnterprise and self.name_ok:
self.CurrentEnterprise["名字"] = data
self.name_ok = False
if self.is_tick:
old_attr_name = self.attr_name
self.attr_name = data
if self.is_tick_data:
self.CurrentEnterprise[old_attr_name] = data
self.is_tick = False
self.is_tick_data = False
pass
pass
def parse_page(htmldata):
"""
获取企业博客的 名字、原创数、粉丝数、获赞数
"""
csdn_parser = CsdnHtmlParser()
csdn_parser.feed(htmldata)
import pprint
pprint.pprint(csdn_parser.enterprises)
def cut_html(htmldata):
import re
# 记住.*?这样就不是贪婪匹配了
pattern = r'.*?<div class="enterprise_r">(.*?)</p>\n\s+</div>.*?'
# 这里使用re.DOTALL让.可以表示任意字符(包括回车换行符) 不加的话.不能表示回车换行符的
# 使用findall找出所有匹配到的内容
matchs = re.findall(pattern, htmldata, re.DOTALL)
for text in matchs:
# print(text)
cut_parser = CutCsdnHtmlParser()
cut_parser.feed(text)
cut_parser.blog_info()
class CutCsdnHtmlParser(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
# 记录一些信息以便后面事件驱动时使用
self.href=""
self.name=""
self.blog = {}
self.name_ok = False
self.is_tick = False
self.attr_name = None
self.is_tick_data = None
def handle_starttag(self, tag, attrs):
if tag == "a" and (("target", "_blank") in attrs):
self.href=list(filter(lambda x: x[0] == "href", attrs))[0][1]
self.name_ok=True
elif tag == "span" and (("class", "name") in attrs):
self.is_tick = True
elif tag == "span" and (("class", "number") in attrs):
self.is_tick_data = True
pass
def handle_startendtag(self, tag, attrs):
pass
def handle_endtag(self, tag):
pass
def handle_data(self, data):
if self.name_ok:
self.name=data
# 避免下次进来替换掉正确的值
self.name_ok=False
elif self.is_tick:
self.attr_name=data
self.is_tick=False
elif self.is_tick_data:
self.blog[self.attr_name]=data
self.is_tick_data=False
pass
def blog_info(self):
self.blog["href"]=self.href
self.blog["name"]=self.name
import pprint
pprint.pprint(self.blog)
pass
if __name__ == "__main__":
url = ""
# 第一种方式是直接整个HTML文件 由于HTMLParser是事件驱动类型 代码写的会比较凌乱
htmldata = get_page(url)
parse_page(htmldata)
# 第二种方式是首先用re模块去匹配出所需要的html内容,然后再通过HTMLParser去解析
# 由于已经通过re模块找出我们所需要的内容,所以代码上相较于第一种方式会简单一点
htmldata = get_page(url)
cut_html(htmldata)