Python爬虫总结
最近临时写了个 python 爬虫的例子(核心代码不开源),总结下这次编写过程中的一些相关知识点与注意事项,以一个用 nodejs 写爬虫的开发者的角度去看待与比对。
编码
在抓包与协议复现的时 候,出现中文以及特殊符号免不了 url 编码,python 的编码可以使用内置库 urllib,同时也能指定编码格式。
gbk 编码中文是占 2 个字节,utf8 编码中文占 3 个字节
url 编码
from urllib.parse import urlencode, parse_qs, quote, unquote
quote("愧怍", encoding="gbk")
# %C0%A2%E2%F4
quot 还有一个 safe 参数,可以指定那个字符不进行 url 编码
quote("?", safe=";/?:@&=+$,", encoding="utf8")
# ? 加了safe
# %3F 不加safe
解码操作与编码同理
unquote("%C0%A2%E2%F4", encoding="gbk")
# 愧怍
如果编码格式错误,比如 gbk 编码用 utf8 解码将会变成不可见字符 ����,而用 utf8 编码用 gbk 解码,存在一个字节差,会输出成其他字符串,比如 你好
就会变成 浣犲ソ
,代码 unquote(quote("你好",encoding='utf8'), encoding="gbk")
URL 查询字符串
如果想构造一个 a=1&b=2
的 url 查询字符串,使用文本拼接很不现实。urllib 提供 urlencode 与 parse_qs 可以在查询字符串与字典中切换
urlencode({
"username": '愧怍',
"password": 'a123456'
})
# username=%E6%84%A7%E6%80%8D&password=a123456
也有 encoding 与 safe 参数,配置同 quote,就不演示了。
parse_qs('a=1&a=2&b=2')
# {'a': ['1', '2'], 'b': ['3']}
将查询字符串转为 python 字典的话,值都是列表(应该是考虑可能会多个相同参数才这么设计)
小提一下,nodejs 中有个 querystring,方法 parse 与 stringify 与效果同理。
解构赋值
a,b = [1,2]
print(a,b)
user = {
"username": "kuizuo",
"password": "a123456"
}
username, password = user.values()
print(username, password)
print(user.keys())
# dict_keys(['username', 'password'])
print(user.values())
# dict_values(['kuizuo', 'a123456'])
解构赋值没什么好说的,和 js 类似,只不过对字典的解构赋值的话,要取值则要调用 values(),取 key 的话默认不填,但是也可以调用 keys()
模板字符串
user = 'kuizuo'
print(f'username: {user} age: {20+1}')
# username: kuizuo age: 21
同样中可以编写表达式,与 js 的模板字符串类似
如果是 python3.6 之前的话,则是用使用 string.format 方法(不常用,也不好用)
"username: {} age: {}".format("愧怍", 18)
而 js 中的模板字符串则是使用反引号`和$,像下面这样
user = 'kuizuo'
console.log(`username: ${user} age: ${20+1}`)
# username: kuizuo age: 21
字典
python 的字典与 js 的对象有些许相像,个人总体感觉没有 js 的对象灵活,演示如下
user = { 'username':'kuizuo','password':'a123456' }
print(user['username'])
想要获取字典中的值,就需要写成user['username']
,如果习惯了 js 的写法(比如我),就会习惯的写成user.username
,这在 python 中将会报错,AttributeError: 'dict' object has no attribute 'username'
,并且字典的 key 还需要使用引号进行包裹,如果是 js 的话,代码如下
user = { username: 'kuizuo', password:'a123456'
console.log(user.username)
如果想在 key 中包裹引号也是可以的,省略引号相当于代码简洁,同时取值也可以像 python 中的user['username']
来进行取值,相对灵活。
假设我想取 user 的 age 属性,但是 user 没有 age 属性,python 则是直接报错KeyError: 'age'
,可以使用user.get('age',20)
,如果没有 age 属性,则默认 20。而 js 是不会报错,则是会返回undefiend
,如果想要默认值的话可以像这样,user.age || 20
。毕竟 js 调用类的方法属性都是可以直接 对象.属性
对象.方法
,而 python 中是 对象["属性"]
对象.方法
,只能说各有各的优劣吧 。
不过 js 不确定是否有该属性的话,可以使用?.
,比方user?.age
,这样返回的null
,而不是undefiend
。
字典['属性值']
获取,key 需用引号包裹类
在写爬虫时,我都会将其封装成类,把一些核心的方法封装成类方法,比如登录,获取图片验证码等等
class Demo():
def __init__(self, user):
self.user = user
def get_img_code(self):
pass
def login(self):
pass
def get_xxx(self):
pass
同样的,像 requests 的 session 也会将其封装在类属性下,但是我一开始的写法是
class Demo():
session = requests.Session()
def __init__(self, user):
self.user = user
导致我创建多个实例时
demo1 = Demo()
demo2 = Demo()
demo1 与 demo2 的的 session 是相等的,经过百度,了解到这样定义的类属性相当于是共有属性,每个实例下获取到的都是同一个 session,如果将 session 放置在__init__
下,每个实例的 session 就不相同
class Demo():
def __init__(self, user):
self.session = requests.Session()
self.user = user
其中 __init__
相当于 js 中的 constructor,也就是构造函数了。
不过 python 的方法第一个参数都要是 self,像 js 或者 java 等一些面向对象的语言,不用特意声明 this,就可以直接使用 this 来调用自身属性与方法。而 python 则需要显式的声明 self。
Python 为什么要保留显式的 self ? - 知乎 (zhihu.com)
线程
python3 中线程操作可以使用 threading
import threading
def func(name, sec):
print('---开始---', name, '时间', ctime())
sleep(sec)
print('***结束***', name, '时间', ctime())
# 创建 Thread 实例
t1 = Thread(target=func, args=('第一个线程', 1))
t2 = Thread(target=func, args=('第二个线程', 2))
# 启动线程运行
t1.start()
t2.start()
# 等待所有线程执行完毕
t1.join() # join() 等待线程终止,要不然一直挂起
t2.join()
多线程
如果要实现多线程的话,需要将 Thread 实例(线程句柄),保存到列表中,然后调用 join
l = []
for i in range(10): # 开启10个线程
t = threading.Thread(target=func, args=('第'+str(i)+'个线程', i))
t.start()
l.append(t)
# 等待线程运行结束
for i in l:
i.join()
锁
说到多线程,怎么可能不提到锁呢。
import threading
from time import sleep
def func():
global num
sleep(1)
lock.acquire() # 获取锁
num = num+1
print(num)
lock.release() # 释放锁
lock = threading.Lock()
num = 0
for i in range(10):
t = threading.Thread(target=func, args=())
t.start()
获取与释放锁的操作可以使用 with 关键字来操作
with lock:
num = num+1
print(num)
时间
计算两者时间间隔
duringtime = datetime.datetime.strptime('2022-03-02 16:16:16', "%Y-%m-%d %H:%M:%S") - datetime.datetime.now()
seconds = duringtime.seconds
定时任务
8 种 Python 定时任务的解决方案 - 知乎 (zhihu.com)
http 请求库
python 较为知名的 http 请求库无非 就是 requests 了,但是 requests 不支持异步,在某些情况下,就只能等待上条请求结束,而异步请求则可以在发起一次请求后,在等待网站返回结果的时间里,可以继续发送更多请求。
此外还有 aiohttp、httpx,由于 httpx 又可以发送同步,也可以发送异步请求,号称新下一代网络请求库,并且基本与 requests 的代码重合度高,只需要改点对应关键词即可,这里所使用的时 httpx,并着重针对两者的区别进行测试。
cookies
在 requests 中想要在下次使用上次响应中返回 cookies 十分简单,只需要设置实例化一个 session,然后使用 session 来发送后续的请求。在 requests 中是session = requests.Session()
,而 httpx 则是client = httpx.Client()
来代替
不过 httpx 则是有同步客户端与异步客户端,下面就是异步请求对的演示代码
import asyncio
import httpx
async def main():
async with httpx.AsyncClient() as client: # 创建一个异步client
r = await client.get('https://www.example.com/')
print(r)
if __name__ == '__main__':
asyncio.run(main())
获取请求的 cookies 也比较简单
request
cookies_dict = requests.utils.dict_from_cookiejar(session.cookies)
httpx
cookies_dict = dict(self.client.cookies)