04反反爬虫和异步爬虫

AffettoIris 2023-3-7 5,460 3/7

验证码

有些网页是只有用户登陆后才能查看的,例如用户的个人信息主页、QQ好友有谁。登录可能需要输入验证码。

识别验证码的方式

  • 人工肉眼识别。将验证码以图象等方式返回给程序员,程序员用眼识别后输入验证码。不推荐,效率低,且没人想识别谷歌那种难以辨识的验证码。但是对于我这种只想下载点小视频、小文档的人来说,要不直接肉眼观察算了。。。据说人眼识别10行代码就好了,使用input传入结果:直接用opencv读取图片,easygui模块显示图片手动输入验证码。真量大了再去搞自动识别

  • 第三方自动识别。晚点再研究免费的破解之道。

    1. 云打码平台已倒闭

    2. 超级鹰,还可以用

    3. Python OCR库如pytorch、buok、ddddocr、tesserocr、pytesseract可能有用;简单的验证码用tesseractocr吧,免费的 ;tessocr要训练,否则再难一些的验证码精度就有点低了

    4. 用别人训练好的模型就行,不用自己训练。斐斐打码验证好使

    5. 可能有其他的python的识别验证码的库

    6. 去图鉴,1块钱可以识别500次,脚本只要填用户名,密码,文件路径

    7. 自己培养深度学习模型:为了不花几块钱,花大把时间学深度的童鞋太可耐了。

    8. 有的网站如古诗词网的验证码是动态变化的(古诗词网获得验证码之后的第二次请求会刷新验证码。导致最后登录请求的验证码刷新了,跟前面下载识别的验证码不一样),这种验证码可以使用selenium模块的截图,然后识别,不然会识别的验证码不符合,已经踩坑。

网站跳转页面的两类方式

  • 对login.php传入参数如密码,login.php返回的响应包是登陆成功后的主页的源码文档,从而实现跳转

  • 对login.php传入参数如密码,login.php返回的响应包不含登陆成功后的主页的源码文档,比如重定向实现的页面跳转

如何反图形验证码我学的不是很精,用到再说吧

利用cookie免登录

http/https是没有记忆的,服务器必须通过cookie、session才能确定刚从登陆前页面到登陆后的页面是同一个人,cookie的作用就是告诉服务器我登陆了,给我看些登陆后的页面,否则服务器不知道你发起的请求是处在已登陆还是未登录状态。可见cookie是查看登录后的页面的必备属性。

请求包如何夹带cookie

手动抓取cookie

response = requests.get(url=url, headers={
    'cookie': 'xxx'  
})  # 不建议,因为一要手动抓cookie,二有的网站的cookie是动态变化的(会过期)

实例

# 由于人人网变了,我又找不简单的适合练手的网站,故拿sqlxy做实验
grade_url = 'http://sqlxy.pro/php/Student/MyGrades.php'
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0',
    'Cookie': 'PHPSESSID=bloomdb906slv06i9j8p0hb73j'  # 如果我不带这个cookie,我得到的页面就没有我的昵称信息
}
response = requests.get(url=grade_url, headers=headers)
response.encoding = response.apparent_encoding
with open('./temp1/sqlxy_grade.html', 'w', encoding=response.apparent_encoding) as file:
    file.write(response.text)
    print("成绩页面下载完毕!")

缺点分析

  • 需要手动抓取cookie。也就意味着需要人工登录一次

  • cookie会失效,那下个月使用脚本岂不是要重新抓cookie么

自动抓取cookie

cookie值的来源是哪里?

模拟登录时对login.php进行post/get请求后,由服务器端创建。

python中创建一个session会话对象:

  1. 作用一,可以进行请求的发送,即用session.get()取代requests.get()

  2. 如果请求过程中产生了cookie,则该cookie会被自动存储/携带在该session对象中,这个功能特性是requests库设计者所作。由于这个特性,即使今年写的代码,明年运行,cookie都是最新的。

  3. 携带了cookie的session对象进行session.get() / session.post()时自动对请求包传入cookie

创建一个session对象: session = requests.Session()  # 首字母大写可见Session是个类

实战

url = 'http://sqlxy.pro/php/LoginRegister/StuLogRegJudeg.php?choose=login'
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0'
}
data = {
    'studentNumber': "25",
    'studentPwd': "111111",
    'agree_cbx': "on",
    'studentLogin': ""
}
session = requests.session()  # 创建session对象
response = session.post(url=url, headers=headers, data=data)  # 用session进行请求,请求过程中如果产生了cookie,session会自动吸收该cookie
print(response.status_code)  # 为什么请求两次,先对后端登录文件发起请求,然后才请求登陆后的信息页?:因为第一次请求能产生cookie

grade_url = 'http://sqlxy.pro/php/Student/MyGrades.php'
response = session.get(url=grade_url, headers=headers)  # 用session进行请求
response.encoding = response.apparent_encoding
with open('./temp1/sqlxy_grade.html', 'w', encoding=response.apparent_encoding) as file:
    file.write(response.text)
    print("成绩页面下载完毕!")  # 得到的登陆后页面是有个人信息的页面

发现

之前说有的网站给的网页是在服务器上组装好后发给客户端,所以你的爬虫能爬到属于动态的区域的内容,发现我大创网站的sqlxy.pro用的php就可以做到,例如主页html源码写着

<i>welcome <?php echo $_SESSION['name'] ?>!</i>
那么爬虫爬到的不是welcome !而是welcome 张三!

代理

有的服务器会统计某个ip在某一段时间内的请求次数,如果超过阈值,客户端就会看到“您的IP访问次数过多”。代理的作用就是破解IP这种反爬机制。代理需要代理服务器,原本我和网站是点对点直接通信,现在安排一个中间人“代理服务器”,我的请求都发给代理,代理为我转发请求给网站,网站的响应因此会发给代理,代理把回复转发给我。

代理的作用

  1. 突破IP访问限制,包括翻墙、短时间内用多个IP请求多次以突破单个IP访问次数的限制

  2. 可以隐藏自身真实的IP

代理服务提供商

  • 快代理

  • 西祠代理

  • www.goubanjia.com

  • 每当我用一元机场翻墙时,我好像就已经在开代理了。应该就是的,ipinfo都检测ip在国外了。但是,理想的代理是能随意调用,比如一个程序中多线程,每个线程一个IP

如何开代理

headers = {    略过   }
params = {    略过    }
proxies = {
    "http://": "103.125.173.14:8080",  # 代理分http型和https型,分别只能访问http://、https://网站
    "https://": "103.125.173.14:8080"
}
text = requests.get(url='url', headers=headers, proxies=proxies).text

代理这块不学了

我需求仅仅是批量下载视频、文档,不用专业,对我个人来说足够了,而且高匿名代理需要money,“哪些免费代理,你能登录上,算你运气好,还想要高匿???“

高性能异步爬虫

单线程,下载是很慢的。

异步爬虫的方式

1. 多线程, 多进程(此方法不建议,有更好的)

  • 好处:可以为相关阻塞的操作单独开启线程或者进程,阻塞操作就可以异步执行。

  • 弊端:无法无限制的开启多线程或者多进程。比如CPU也就几核;需要频繁地创建和销毁进程、线程

线程池与多线程的区别

  • 线程池是在程序运行开始,创建好的n个线程,并且这n个线程挂起等待任务的到来。而多线程是在任务到来得时候进行创建,然后执行任务。

  • 线程池中的线程执行完之后不会回收线程,会继续将线程放在等待队列中;多线程程序在每次任务完成之后会回收销毁该线程。

  • 由于线程池中线程是创建好的,所以在效率上相对于多线程会高很多。

  • 线程池也在高并发的情况下有着较好的性能;不容易挂掉。多线程在创建线程数较多的情况下,很容易挂掉。

2.线程池、进程池(适当的使用)

  • 好处:我们可以降低系统对进程或者线程创建和销毁的一个频率,从而很好的降低系统的开销。

  • 弊端:池中线程或进程的数量是有上限。

线程池案例实例

import time
from multiprocessing import Pool  # 大写开头,说明是个类,不是函数
def get_str(str):
    print("开始下载" + str)
    time.sleep(2)
    print("结束下载" + str)
if __name__ == '__main__':
    start_time = time.time()  # 介绍写着:Return the current time in seconds

    name_list = ['aaa', 'bbb', 'ccc', 'ddd']
    pool = Pool(4)  # 实例化一个线程池对象,创建4个线程
    pool.map(get_str, name_list)  # 将name_list中的每个元素传递给get_str处理
    # 由于pool有四个线程池,所以四个线程池会各分一个任务;所以pool.map()的返回值是get_str们的返回值组成的列表
    end_time = time.time()
    print("总耗时=" + str(end_time - start_time))  
开始下载aaa
开始下载bbb
开始下载ccc
开始下载ddd
结束下载aaa
结束下载bbb
结束下载ddd结束下载ccc

总耗时=2.1397335529327393

遇到过的两个报错:

  • RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase. This probably means that you are not using fork to start your child processes and you have forgotten to use the proper idiom in the main module:

    原因:基于线程池的多进程需要在if name == 'main':中运行,每次线程运行都会运行import time所在的空间的所有代码和mp_main下空间的所有代码

  • AttributeError: pool.map() Can‘t get attribute ‘func‘ on <module ‘__mp_main__‘ from ‘...‘>
    报错原因:因为在子进程中,__name__ 等于 "__mp_main__" 而不是 __main__,所以 if __name__ == "__main__": 下的代码不会被运行,所以不要把func函数定义在main函数下,不然会导致对于线程来说 func 函数未定义。

不能把所有阻塞的业务都给线程池,线程池应处理阻塞且耗时的任务,阻塞但不严重耗时的任务就算了。

老师爬取一个视频的url,结果爬到的为空,打开源码一看,原来url是js设置的,而爬虫爬取的页面又没有js引擎执行js,所以url为空。解决方法是找到该js文件,由于bs4和xpath是针对html源码的,所以这里用正则匹配

3.单线程+异步协程(推荐)

一些概念

event_ loop: 事件循环,相当于一个无限循环, 我们可以把一些函数注册到这个事件循环上,

当满足某些条件的时候,函数就会被循环执行。

coroutine:协程对象,我们可以将协程对象注册到事件循环中,它会被事件循环调用。

async:用于定义一个协程.(起码得python3.6才支持该关键字)。我们可以使用async关键字来定义一个方法,被async修饰的方法在调用时不会立即被执行,而是返回一个协程对象coroutine。只有当该coroutine注册到事件循环中才会执行函数体

task:任务,它是对coroutine的进一步封装, 包含了任务的各个状态。

future:代表将来执行或还没有执行的任务,实际上和task 没有本质区别。

await:当遇到某个阻塞的方法时可以用await挂起阻塞方法的执行。

单任务协程实例

步骤
  • 用待执行函数创建协程对象coroutine并封装成task

  • 创建事件循环对象loop

  • 调用loop.run_until_complete(task)或loop.run_until_complete(asyncio.wait(tasks_list))正式执行函数

import asyncio  # 反例
async def request(url):
    print(url)
request('123')  # RuntimeWarning: coroutine 'request' was never awaited
import asyncio  # 正例,没封装,不用学
async def request(url):  # async修饰的函数,调用后返回一个协程对象
    print(url)
coroutine = request('123')  # 被async修饰的方法在调用时不会立即被执行
loop = asyncio.get_event_loop()  # 创建一个事件循环对象
loop.run_until_complete(coroutine)  # 将协程对象注册到loop中,然后启动loop,此时才执行函数体
import asyncio # 学 # 将coroutine封装成task,以实时观看任务 在过程中/执行完毕 的状态
async def request(url):
    print(url)
coroutine = request('123')
task = loop.create_task(coroutine)  # 将coroutine封装成task
loop = asyncio.get_event_loop()  # 创建一个事件循环对象
print(task)  # <Task pending ...
loop.run_until_complete(task)  # 如果本行是loop.run_until_complete(coroutine),会报错RuntimeError: cannot reuse already awaited coroutine
print(task)  # <Task finished ...
import asyncio # 不看  # 将coroutine封装成future,以实时观看任务 在过程中/执行完毕 的状态
import time
async def request(url):  # async修饰的函数,调用后返回一个协程对象
    print(url)
coroutine = request('123')
loop = asyncio.get_event_loop()  # 创建一个事件循环对象
future = asyncio.ensure_future(coroutine)  # 将coroutine封装成future
print(future)  # <Task pending ...
loop.run_until_complete(future)  # 可见future和task的书写步骤、输出结果都是一样的,只是方法名和方法归属哪个类不一样
print(future)  # <Task finished ...

总结:task和future除了方法名和方法归属的类,没区别,会用一个即可。

给task绑定callback()使得task执行完正主函数后再执行回调函数

该回调函数可以调用task.result(),返回值取自正主函数的返回值

import asyncio
async def request(url):
    print(url)
    return url

def callback(task):  # 回调函数需要有个代表task的参数作为输入
    print('我来自回调函数:', task.result())  # 直译,任务对象task的结果,而task本质是coroutine,而coroutine来自request(),所以task.result()意指request()的返回值
    
coroutine = request('123')
loop = asyncio.get_event_loop()
task = loop.create_task(coroutine)  # task和future都可以,一样的
task.add_done_callback(callback)  # 绑定回调,寓意task添加任务执行完毕后的回调函数。作用是当coroutine的函数执行完毕后执行callback();这里只取了方法名做参数,说明task会自动传进去
loop.run_until_complete(task)
# 输出:123
# 我来自回调函数: 123

多任务异步协程实现

上面的实例都是单任务,即我们只制作了一个coroutine,将这一个协程对象封装到了一个任务对象中去,将这一个任务对象注册到了事件循环中去,最终事件循环只注册了一个任务对象task。

基于异步协程的爬虫不能使用任何一点同步模块相关的代码,包括time.sleep(2)、requests.get()、requests.post(),可以用asyncio.sleep(2)、aiohttp.get()、aiohttp.post()替代。此外阻塞且耗时的代码需要用await挂起

import asyncio
import time
urls = ['a', 'b', 'c']
async def request(url):
    print("Downloading " + url)
    time.sleep(2)
    print(url + " Over!")

start_time = time.time()
tasks_list = []  # 任务列表
loop = asyncio.get_event_loop()

for url in urls:
    coroutine = request(url)
    task = loop.create_task(coroutine)
    tasks_list.append(task)

loop.run_until_complete(asyncio.wait(tasks_list))  # tasks_list是任务列表,不是单个的任务了,你不能直接放如参数中
print("""共用时""", time.time() - start_time)  # 失败了,没有异步
# 输出Downloading a
# a Over!
# Downloading b
# b Over!
# Downloading c
# c Over!
# 共用时 6.003433465957642

分析原因后改进:重新定义上面代码中的request(url),其他不变

await关键字

await可以让程序员手动地对阻塞且耗时的操作如IO操作,当线程执行到这行代码时挂起该线程,告诉CPU转去忙别的线程,直到IO完毕便回来继续执行。 await是使协程能异步执行的关键

async def request(url):
    print("Downloading " + url)
    # 原因:在异步协程中如果出现了任何一点同步模块相关的代码,那么就无法实现异步。time.sleep(2)属同步模块的代码
    # 当在asyncio中遇到阻塞操作必须手动挂起
    await asyncio.sleep(2)  # 改进:用asyncio.sleep(2)取代time.sleep(2)
    print(url + " Over!")
# 输出Downloading a
# Downloading b
# Downloading c
# a Over!
# b Over!
# c Over!
# 共用时 2.0113368034362793

多任务异步协程实现异步爬虫

import time
import requests
import asyncio
urls = ['http://127.0.0.1:5000/bobo', 'http://127.0.0.1:5000/jack', 'http://127.0.0.1:5000/tom']

async def get_page(url):
    print("Downloading " + url)
    requests.get(url)
    print("Downloading " + url + 'over!')

loop = asyncio.get_event_loop()
tasks_list = []
for url in urls:
    coroutine = get_page(url)
    task = loop.create_task(coroutine)
    tasks_list.append(task)

start_time = time.time()
loop.run_until_complete(asyncio.wait(tasks_list))
print("总耗时:", time.time() - start_time)  # 总耗时: 6.0259785652160645
# 可见没有异步

分析原因并改进

原因:requests.get()基于同步,我们需改用基于异步的网络请求模块来请求。aiohttp正是一个基于异步的网络请求模块,我们需要使用aiohttp模块的ClientSession()类。

改进:用aiohttp.ClientSession()的get(url)替换requests.get(url),其他不变

import aiohttp
async def get_page(url):
    async with aiohttp.ClientSession() as session:  # 固定语法 async with as:
        async with await session.get(url) as response:  # 由于本行的get()方法是个耗时且阻塞,所以需要挂起,所以加上await
            # session.get(url)和requests.get(url)的用法上区别不大
            # requests.get(url).text <==> session.get(url).text()
            # requests.get(url).json <==> session.get(url).json()
            # requests.get(url).content <==> session.get(url).read()
            page_text = response.text()
            print(page_text)  # 总耗时: 2.0064821243286133,但是报了个错:RuntimeWarning: coroutine 'ClientResponse.text' was never awaited

原因:从操作系统的角度分析,page_text = response.text()是个IO操作,而IO是个阻塞且耗时的操作,如果放任不管,处理器会顺序操作,即干等IO完毕再往下执行。

await是一个只能在协程函数中使用的关键词,用于在遇到IO操作时悬挂当前协程(任务),可以让处理器遇到IO操作时,先去忙别的事,等这边IO完毕再切换过来执行后续代码。

async def get_page(url):
    async with aiohttp.ClientSession() as session:
        async with await session.get(url) as response:
            page_text = await response.text()
            print(page_text)  # 总耗时: 2.0064821243286133 无报错

aiohttp的扩展

async with aiohttp.ClientSession() as session:
    await session.get(url) <==> requests.get(url)
    await session.post(url) <==> requests.post(url)
    如果你需要夹带headers、params、data,用法和requests的用法一样
    如果需要夹带代理,session.get()/post()的代理只能是字符串而非字典
    await session.post(url, proxy='http://ip:port') <==> requests.post(url, proxies={
        'http://': "ip:port"
    })
- THE END -

AffettoIris

10月16日16:06

最后修改:2023年10月16日
0

非特殊说明,本博所有文章均为博主原创。

共有 0 条评论