requests、httpx、curl_cffi、pycurl、you-get、wget、ffmpeg、下载(图片、音乐、视频)、websockets


 

1、requests 的使用

Requests 官方文档:http://cn.python-requests.org/zh_CN/latest

反检测 requests (stealth-requests)

pypi 搜索:stealth

stealth-requests:https://pypi.org/project/stealth-requests/

Web Scraping 的多合一解决方案:

实际的 HTTP 请求:

  • 模拟浏览器标头以进行未检测到的抓取,适应请求的文件类型
  • 跟踪动态标头,例如:Referer、Host
  • 使用 curl_cffi 包屏蔽 HTTP 请求的 TLS 指纹

更快、更轻松的解析:

  • 从基于 HTML 的响应中自动提取元数据(标题、描述、作者等)
  • 提取所有网页和图像 URL 的方法
  • 将响应无缝转换为 Lxml 和 BeautifulSoup 对象

安装:pip install stealth_requests

Stealth-Requests 模拟了 requests 包的 API,允许以几乎相同的方式使用它。

发送性请求:

import stealth_requests as requests

resp = requests.get('https://link-here.com')

使用 Asyncio 发送请求

from stealth_requests import AsyncStealthSession

async with AsyncStealthSession(impersonate='safari') as session:
    resp = await session.get('https://link-here.com')

对于一次性请求,您可以发出如下请求:

import stealth_requests as requests

resp = await requests.get('https://link-here.com', impersonate='safari')

返回的是一个响应,它具有与标准 requests 响应对象相同的方法和属性,并添加了一些功能。这些额外功能之一是自动解析基于 HTML 的响应中的标头元数据。可以从 property 访问元数据

import stealth_requests as requests

resp = requests.get('https://link-here.com')
print(resp.meta.title)

为了更快地解析 HTML,Stealth-Requests 添加了两个流行的解析包 Lxml 和 BeautifulSoup4。要使用这些附加组件需要安装 parsers。安装:pip install stealth_requests[parsers]

  • 获取 lxml tree:resp.tree()
  • 获取 BeautifulSoup 对象:resp.soup()

为了进行简单的解析,Lxml 包中还直接添加了 StealthResponse对象 作为便捷方法。

  • text_content():获取响应中的所有文本内容
  • xpath() 直接使用 XPath 表达式,而不是获取自己的 Lxml 树。

从响应中获取所有图像和页面链接

import stealth_requests as requests

resp = requests.get('https://link-here.com')
for image_url in resp.images:
    # ...

获取 Markdown 格式的 HTML 响应。在某些情况下,使用 Markdown 格式的网页比使用 HTML 更容易。在发出返回 HTML 的 GET 请求后,可以使用 resp.markdown() 方法将响应转换为 Markdown 字符串,从而提供简化且可读的页面内容版本

markdown()有两个可选参数

  • content_xpath:XPath 表达式。用于缩小转换为 Markdown 的文本范围。
  • ignore_links:一个布尔值,告知 Html2Text 是否应在 Markdown 的输出中包含任何链接。

用户指南

从 Requests 的背景讲起,然后对 Requests 的重点功能做了逐一的介绍。

API 文档/指南

如果你要了解具体的函数、类、方法,这部分文档就是为你准备的。

示例:简单使用

安装:pip install requests

import json
import requests

# HTTP 请求类型
r = requests.get('https://github.com/timeline.json')  # get 类型
r = requests.post("http://m.ctrip.com/post")  # post 类型
r = requests.put("http://m.ctrip.com/put")  # put 类型
r = requests.delete("http://m.ctrip.com/delete")  # delete 类型
r = requests.head("http://m.ctrip.com/head")  # head 类型
r = requests.options("http://m.ctrip.com/get")  # options类型

# 获取响应内容
print(r.content)  # 以字节的方式去显示,中文显示为字符
print(r.text)  # 以文本的方式去显示

payload = {'keyword': '日本', 'salecityid': '2'}
# 向 URL 传递参数
r = requests.get("https://m.ctrip.com/webapp/tourvisa/visa_list", params=payload)
print(r.url)

# 获取/修改网页编码
r = requests.get('https://github.com/timeline.json')
print(r.encoding)
# 修改网页编码
r.encoding = 'utf-8'

# json处理
r = requests.get('https://github.com/timeline.json')
print(r.json())  # 需要先 import json

# 定制请求头 (get 和 post 都一样方式)
url = 'https://m.ctrip.com'
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
                  '(KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.62'
}
r = requests.post(url, headers=headers)  # 或者 r = requests.get(url, headers=headers)
print(r.request.headers)


# 复杂post请求
url = 'https://m.ctrip.com'
payload = {'some': 'data'}
# 如果传递的payload是string而不是dict,需要先调用dumps方法格式化一下
r = requests.post(url, data=json.dumps(payload))

# post 多部分编码文件
url = 'https://m.ctrip.com'
files = {'file': open('report.xls', 'rb')}
r = requests.post(url, files=files)


r = requests.get('http://m.ctrip.com')
# 响应状态码
print(r.status_code)

r = requests.get('http://m.ctrip.com')
# 响应头
print(r.headers)

print(r.headers['Content-Type'])
# 访问响应头部分内容的两种方式
print(r.headers.get('content-type'))

# Cookies
url = 'https://example.com/some/cookie/setting/url'
r = requests.get(url)
r.cookies['example_cookie_name']  # 读取 cookies

url = 'https://m.ctrip.com/cookies'
cookies = dict(cookies_are='working')
r = requests.get(url, cookies=cookies)  # 发送cookies

# 设置超时时间
r = requests.get('https://m.ctrip.com', timeout=0.001)

# 设置访问代理(get 和 post 都一样方式)
proxies = {
    "http": "http://10.10.10.10:8888",
    "https": "http://10.10.10.100:4444",
}
r = requests.get('https://m.ctrip.com', proxies=proxies)

发送 get 请求、传递参数

import requests

r = requests.get("http://httpbin.org/get")
print(type(r))
print(r.status_code)  # 获取响应状态码
print(r.encoding)  # 获取网页编码
print(r.text)  # r.text来获取响应的内容。以字符的形式获取
print(r.content)  # 以字节的形式获取响应的内容。requests会自动将内容转码。
print(r.cookies)  # 获取cookies

requests.get('https://github.com/timeline.json')   # GET请求
requests.post('https://httpbin.org/post')           # POST请求
requests.put('https://httpbin.org/put')             # PUT请求
requests.delete('https://httpbin.org/delete')       # DELETE请求
requests.head('https://httpbin.org/get')            # HEAD请求
requests.options('https://httpbin.org/get')         # OPTIONS请求

requests 模块

  • 发送 get 请求时,请求参数可以直接放在 ur1 的?后面,也可以放在字典里,传递给params
  • 发送 post 请求时,请求参数要放在字典里,传递给 data
import requests

payload = {'key1': 'value1', 'key2': 'value2'}
r = requests.get("http://httpbin.org/get", params=payload)
print r.url

运行结果

http://httpbin.org/get?key2=value2&key1=value1
url = "https://www.baidu.com/s"
r = requests.get(url, params={'wd': 'python'})
print r.url
r = requests.get(url, params={'wd': 'php'})
print r.url
print r.text

示例:带参数的请求

import requests

# GET参数实例
requests.get('https://www.baidu.com/s', params={'wd': 'python'})
# POST参数实例
requests.post(
    'https://www.itwhy.org/wp-comments-post.php',
    data={'comment': '测试POST'}
)

# get 方法 使用 params 参数传递数据
# post 方法 使用 data 参数传递数据

r = requests.get("https://httpbin.org/get", params={'wd': 'python'})  # get 参数示例
print(r.url)
print(r.text)

# post 方法 如果使用 params 参数传递数据时,传递的数据可以在url中以明文看到
r = requests.post("https://httpbin.org/post", params={'wd': 'python'})
print(r.url)
print(r.text)

# post 如果使用 data 参数传递数据时,传递的数据在url中无法看到
r = requests.post(
    "https://httpbin.org/post", 
    data={'comment': 'TEST_POST'}
)  # post 参数示例
print(r.url)
print(r.text)

如果想请求JSON文件,可以利用 json() 方法解析。例如自己写一个JSON文件命名为a.json,内容如下

["foo", "bar", {
  "foo": "bar"
}]

利用如下程序请求并解析

import requests

# a.json 代表的一个服务器json文件,这里为了演示
# 实际是:http://xxx.com/a.json  形式的URL地址
r = requests.get("a.json") 
print(r.text)
print(r.json())

运行结果如下,其中一个是直接输出内容,另外一个方法是利用 json() 方法解析。

["foo", "bar", {
 "foo": "bar"
 }]
 [u'foo', u'bar', {u'foo': u'bar'}]

如果想获取来自服务器的原始套接字响应,可以取得 r.raw 。 不过需要在初始请求中设置 stream=True 。

r = requests.get('https://github.com/timeline.json', stream=True)
r.raw
<requests.packages.urllib3.response.HTTPResponse object at 0x101194810>
r.raw.read(10)
'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03'

这样就获取了网页原始套接字内容。如果想添加 headers,可以传 headers 参数

import requests

payload = {'key1': 'value1', 'key2': 'value2'}
headers = {'content-type': 'application/json'}
r = requests.get("http://httpbin.org/get", params=payload, headers=headers)
print(r.url)

通过headers参数可以增加请求头中的headers信息

requests 的 Streaming 模式

Streaming 模式是一种高效处理大文件或实时数据流的方式。通过设置 stream=True,可以逐块接收 HTTP 响应数据,而不是一次性将整个响应体加载到内存中。这种方式可以显著减少内存占用,并支持即时处理数据。

实现 Streaming 模式的关键步骤

  • 1. 启用 Streaming 模式
  • 2. 读取数据 ( 逐块 / 逐行 )
  • 3. 关闭连接

应用场景

  • 下载大文件:避免一次性加载整个文件到内存中,适合处理超大文件。
  • 实时数据流:如处理日志流、视频流或其他实时更新的数据。
  • 节省带宽:通过按需读取数据,避免不必要的流量消耗。

示例:

import requests

def func_1():
    url = "https://example.com/largefile"
    response = requests.get(url, stream=True)
    # 方法 1:逐块读取数据。使用 iter_content() 方法以块的形式读取响应体。
    # 可以通过 chunk_size 参数指定每次读取的块大小(以字节为单位)。
    with open("large_file.zip", "wb") as file:
        for chunk in response.iter_content(chunk_size=1024):
            if chunk:  # 确保块不为空
                file.write(chunk)

def func_2():
    url = "https://example.com/largefile"
    response = requests.get(url, stream=True)
    # 方法 2:逐行读取数据。如果需要按行处理数据,可以使用 iter_lines() 方法。
    for line in response.iter_lines():
       if line:
           print(line.decode("utf-8"))
    response.close()

使用aiohttp进行异步流式下载的示例:

import aiohttp
import asyncio

async def download_file_async(url, filename):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            response.raise_for_status()
            # 异步写入文件
            with open(filename, 'wb') as f:
                async for chunk in response.content.iter_chunked(8192):
                    f.write(chunk)

# 使用示例
url = 'https://example.com/largefile.zip'
filename = 'largefile.zip'

# 运行异步下载任务
asyncio.run(download_file_async(url, filename))

使用 tqdm 显示下载进度,tqdm 是一个快速、扩展性强的Python进度条库。pip install tqdm

import requests
from tqdm import tqdm

def download_file_with_progress(url, filename):
    # 获取文件总大小
    total_size = int(requests.head(url).headers.get('content-length', 0))

    # 使用requests进行流式下载
    with requests.get(url, stream=True) as r:
        r.raise_for_status()
        with open(filename, 'wb') as f, tqdm(
            desc=filename,
            total=total_size,
            unit='iB',
            unit_scale=True,
            unit_divisor=1024,
        ) as bar:
            for chunk in r.iter_content(chunk_size=8192):
                size = f.write(chunk)
                bar.update(size)

# 使用示例
url = 'https://example.com/largefile.zip'
filename = 'largefile.zip'
download_file_with_progress(url, filename)

自定义 header

import requests
import json
 
data = {'some': 'data'}
headers = {
    'content-type': 'application/json',
    'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:22.0)'
}
 
r = requests.post('https://api.github.com/some/endpoint', data=data, headers=headers)
print(r.text)

请求头内容可以用r.request.headers来获取。

>>> r.request.headers

自定义请求头部:定制 headers,使用 headers 参数来传递

伪装请求头部是采集时经常用的,我们可以用这个方法来隐藏:

r = requests.get('http://www.zhidaow.com')
print(r.request.headers['User-Agent'])

headers = {'User-Agent': 'alexkh'}
r = requests.get('http://www.zhidaow.com', headers = headers)
print(r.request.headers['User-Agent'])

POST 请求

请求参数,观察抓包的参数状况: 

  • QueryStringParameters   --->   url
  • Form Data   --->   requests.post(data)
  • requestpayload   --->  requests.post(data=json.dumps(dict),headers={"contentType":"application/json"})
"""
requests  -> 

    get:
        Query String Parameters  ->  url
        url上拼接? xxx=xxx&xxxx=xxxx
        params -> 也可以把上述参数进行设置
    
    post:
        Form Data
            把字典传递个data即可
            requests.post(url, data=dict)
        Request Payload
            把字典处理成json传递给data
            字典处理成json之后. json是不是字符串????
            同时需要给出请求头中的Content-Type : application/json
"""

对于 POST 请求一般需要为它增加一些参数。最基本的传参方法可以利用 data 这个参数。

import requests

payload = {'key1': 'value1', 'key2': 'value2'}
r = requests.post("http://httpbin.org/post", data=payload)
print(r.text)

执行成功后,服务器返回了我们传的数据。如果需要传JSON格式的数据,可以用 json.dumps() 方法把表单数据序列化。

POST发送JSON数据:

import json
import requests

url = 'http://httpbin.org/post'
payload = {'some': 'data'}
r = requests.post(url, data=json.dumps(payload))
print(r.text)

data 参数

  • 当传递给requests.post方法的data参数是字典类型时,requests会自动将其编码为表单形式(application/x-www-form-urlencoded),这是大多数HTTP表单提交的默认编码方式。
  • 如果data参数是字符串,则可以自定义数据格式,如JSON字符串。此时,你可能需要手动设置headers参数中的'Content-Type''application/json',或其他适合你数据格式的MIME类型。
  • data参数可以是字典、字节序列,或文件对象的元组列表。

json 参数

  • json参数提供了一种更直接的方式来发送JSON编码的数据。当使用json参数时,requests会自动将字典编码为JSON格式,并自动将Content-Type设置为application/json
  • 使用json参数时,无需调用json.dumps()来序列化数据,requests会处理这一过程。

结论

  • 使用data参数更为通用,适用于提交表单数据或发送自定义格式的数据。但要正确发送JSON数据,需要手动设置Content-Type头部。
  • 使用json参数时,适用于发送JSON数据,更为便捷,不需要手动指定Content-Type头部,因为requests会自动处理。

post 请求载体是表单时,则 data参数 是 字典

post 请求载体是载荷时,data参数是字符串,不是字典

import requests
import json

headers = {
    "Content-Type": "application/json",
    "u-sign": "f4f3ddab7f10a7d927fdfef3a7a3ca2d",
}
url = "https://uwf7de983aad7a717eb.youzy.cn/youzy.dms.datalib.api.enrolldata.enter.college.encrypted.v2.get"
data_dict = {
    "collegeCode": "10001",
    "provinceCode": 37
}
data_string = json.dumps(data_dict, separators=(',', ':'))
response = requests.post(url, headers=headers, data=data_string)

print(response.text)
print(response)

一般而言,如果是提交JSON数据给服务器,使用json参数会更加方便。如果提交其他格式的数据或表单数据,使用data参数会更适合。

如果想要上传文件,那么直接用 file 参数即可。新建一个 a.txt 的文件,内容写上 Hello World!

import requests

url = 'http://httpbin.org/post'
files = {'file': open('test.txt', 'rb')}
r = requests.post(url, files=files)
print r.text

运行结果如下

{
  "args": {}, 
  "data": "", 
  "files": {
    "file": "Hello World!"
  }, 
  "form": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Content-Length": "156", 
    "Content-Type": "multipart/form-data; boundary=7d8eb5ff99a04c11bb3e862ce78d7000", 
    "Host": "httpbin.org", 
    "User-Agent": "python-requests/2.9.1"
  }, 
  "json": null, 
  "url": "http://httpbin.org/post"
}

这样便成功完成了一个文件的上传。
requests 是支持流式上传的,这允许你发送大的数据流或文件而无需先把它们读入内存。要使用流式上传,仅需为你的请求体提供一个类文件对象即可。这是一个非常实用方便的功能。

with open('massive-body') as f:
    requests.post('http://some.url/streamed', data=f)
import requests
 
url = 'http://127.0.0.1:5000/upload'
files = {'file': open('/home/lyb/sjzl.mpg', 'rb')}
#files = {'file': ('report.jpg', open('/home/lyb/sjzl.mpg', 'rb'))}     #显式的设置文件名
 
r = requests.post(url, files=files)
print(r.text)

你可以把字符串当着文件进行上传:

import requests
 
url = 'http://127.0.0.1:5000/upload'
files = {'file': ('test.txt', b'Hello Requests.')}     #必需显式的设置文件名
 
r = requests.post(url, files=files)
print(r.text)

发送文件的post类型,这个相当于向网站上传一张图片,文档等操作,这时要使用files参数

>>> url = 'http://httpbin.org/post'
>>> files = {'file': open('touxiang.png', 'rb')}
>>> r = requests.post(url, files=files)

POST 请求模拟登陆及一些返回对象的方法

import requests

url1 = 'https://www.exanple.com/login'  # 登陆地址
url2 = "https://www.example.com/main"  # 需要登陆才能访问的地址
data = {"user": "user", "password": "pass"}
headers = {
    "Accept": "text/html,application/xhtml+xml,application/xml;",
    "Accept-Encoding": "gzip",
    "Accept-Language": "zh-CN,zh;q=0.8",
    "Referer": "https://www.example.com/",
    "User-Agent": ""
}
res1 = requests.post(url1, data=data, headers=headers)
res2 = requests.get(url2, cookies=res1.cookies, headers=headers)
print(res2.content)  # 获得二进制响应内容
print(res2.raw)  # 获得原始响应内容,需要stream=True
print(res2.raw.read(50))
print(type(res2.text))  # 返回解码成unicode的内容
print(res2.url)
print(res2.history)  # 追踪重定向
print(res2.cookies)
print(res2.cookies['example_cookie_name'])
print(res2.headers)
print(res2.headers['Content-Type'])
print(res2.headers.get('content-type'))
print(res2.json)  # 讲返回内容编码为json
print(res2.encoding)  # 返回内容编码
print(res2.status_code)  # 返回http状态码
print(res2.raise_for_status())  # 返回错误状态码

Response 对象

使用requests方法后,会返回一个response对象,其存储了服务器响应的内容,如上实例中已经提到的 r.text、r.status_code……
获取文本方式的响应体实例:当你访问 r.text 之时,会使用其响应的文本编码进行解码,并且你可以修改其编码让 r.text 使用自定义的编码进行解码。

r.status_code    # 响应状态码
r.raw            # 返回原始响应体,也就是 urllib 的 response 对象,使用 r.raw.read() 读取
r.content        # 字节方式的响应体,会自动为你解码 gzip 和 deflate 压缩
r.text           # 字符串方式的响应体,会自动根据响应头部的字符编码进行解码
r.headers        # 以字典对象存储服务器响应头,但是这个字典比较特殊,字典键不区分大小写,若键不存在则返回None

#*特殊方法*#
r.json()                # Requests中内置的JSON解码器
r.raise_for_status()    # 失败请求(非200响应)抛出异常

Cookies

如果一个响应中包含了cookie,那么我们可以利用 cookies 变量来拿到。会话对象让你能够跨请求保持某些参数,最方便的是在同一个Session实例发出的所有请求之间保持cookies,且这些都是自动处理的

import requests

url = 'http://example.com'
r = requests.get(url)
print r.cookies
print r.cookies['example_cookie_name']

以上程序仅是样例,可以用 cookies 变量来得到站点的 cookies。另外可以利用 cookies 变量来向服务器发送 cookies 信息

import requests

url = 'http://httpbin.org/cookies'
cookies = dict(cookies_are='working')
r = requests.get(url, cookies=cookies)
print r.text

运行结果
'{"cookies": {"cookies_are": "working"}}'

如果某个响应中包含一些Cookie,你可以快速访问它们:

import requests

r = requests.get('http://www.google.com.hk/')
print(r.cookies['NID'])
print(tuple(r.cookies))

要想发送你的cookies到服务器,可以使用 cookies 参数:

import requests
 
url = 'http://httpbin.org/cookies'
cookies = {'testCookies_1': 'Hello_Python3', 'testCookies_2': 'Hello_Requests'}
# 在Cookie Version 0中规定空格、方括号、圆括号、等于号、逗号、双引号、斜杠、问号、@,冒号,分号等特殊符号都不能作为Cookie的内容。
r = requests.get(url, cookies=cookies)
print(r.json())

如下是快盘签到脚本

import requests
 
headers = {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
           'Accept-Encoding': 'gzip, deflate, compress',
           'Accept-Language': 'en-us;q=0.5,en;q=0.3',
           'Cache-Control': 'max-age=0',
           'Connection': 'keep-alive',
           'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:22.0) Gecko/20100101 Firefox/22.0'}
 
s = requests.Session()
s.headers.update(headers)
# s.auth = ('superuser', '123')
s.get('https://www.kuaipan.cn/account_login.htm')
 
_URL = 'http://www.kuaipan.cn/index.php'
s.post(_URL, params={'ac':'account', 'op':'login'},
       data={'username':'****@foxmail.com', 'userpwd':'********', 'isajax':'yes'})
r = s.get(_URL, params={'ac':'zone', 'op':'taskdetail'})
print(r.json())
s.get(_URL, params={'ac':'common', 'op':'usersign'})

获取响应中的cookies

获取响应中的cookies
>>> r = requests.get('http://www.baidu.com')
>>> r.cookies['BAIDUID']
'D5810267346AEFB0F25CB0D6D0E043E6:FG=1'

也可以自已定义请求的COOKIES
>>> url = 'http://httpbin.org/cookies'
>>> cookies = {'cookies_are':'working'}
>>> r = requests.get(url,cookies = cookies)
>>> 
>>> print r.text
{
  "cookies": {
    "cookies_are": "working"
  }
}
>>>

超时 配置

可以利用 timeout 变量来配置最大请求时间。timeout 仅对连接过程有效,与响应体的下载无关。

requests.get('http://github.com', timeout=0.001)

注:timeout 仅对连接过程有效,与响应体的下载无关。

也就是说,这个时间只限制请求的时间。即使返回的 response 包含很大内容,下载需要一定时间,然而这并没有什么卵用。

Session

requests 爬虫:http://www.cnblogs.com/lucky-pin/p/5806394.html

在了解 Session 和 Cookie 之前,需要先了解 HTTP 的一个特点,叫作无状态

无状态 HTTP

HTTP 的无状态是指 HTTP 协议对事务处理是没有记忆能力的,或者说服务器并不知道客户端处于什么状态。客户端向服务器发送请求后,服务器解析此请求,然后返回对应的响应,服务器负责完成这个过程,而且这个过程是完全独立的,服务器不会记录前后状态的变化,也就是缺少状态记录。这意味着之后如果需要处理前面的信息,客户端就必须重传,导致需要额外传递一些重复请求,才能获取后续响应,这种效果显然不是我们想要的。为了保持前后状态,肯定不能让客户端将前面的请求全部重传一次,这太浪费资源了,对于需要用户登录的页面来说,更是棘手
这时两种用于保持HTTP连接状态的技术出现了,分别是Session和Cookie。
Session 在服务端也就是网站的服务器,用来保存用户的 Session 信息;
Cookie 在客户端,也可以理解为在浏览器端。有了 Cookie,浏览器在下次访问相同网页时就会自动附带上它,并发送给服务器,服务器通过识别Cookie 鉴定出是哪个用户在访问,然后判断此用户是否处于登录状态,并返回对应的响应。
可以这样理解,Cookie里保存着登录的凭证,客户端在下次请求时只需要将其携带上,就不必重新输人用户名、密码等信息重新登录了
因此在爬虫中,处理需要先登录才能访间的页面时,我们一般会直接将登录成功后获取的 Cookie放在请求头里面直接请求,而不重新模拟登录。

什么是 Session

Session 中文称之为会话,其本义是指有始有终的一系列动作、消息。例如打电话时,从拿起电话拨号到挂断电话之间的一系列过程就可以称为一个Session。
而在Web中Session对象用来存储特定用户Session 所需的属性及配置信息。这样,当用户在应用程序的页面之间跳转时,存储在Session 对象中的变量将不会丢失,会在整个用户Session中一直存在下去。当用户请求来自应用程序的页面时,如果该用户还没有 Session,那么 Web 服务器将自动创建一个Session对象。当Session过期或被放弃后,服务器将终止该Session。

什么是 Cookie

Cookie 指某些网站为了鉴别用户身份、进行Session跟踪而存储在用户本地终端上的数据

Session 维持原理

那么,怎样利用 Cookie 保持状态呢?在客户端第一次请求服务器时,服务器会返回一个响应头中带有 Set-Cookie字段的响应给客户端,这个字段用来标记用户。客户端浏览器会把 Cookie 保存起来,当下一次请求相同的网站时,把保存的 Cookie 放到请求头中一起提交给服务器。Cookie 中携带着SessionID相关信息,服务器通过检查Cookie 即可找到对应的Session,继而通过判断Session辨认用户状态。如果 Session 当前是有效的,就证明用户处于登录状态,此时服务器返回登录之后才可以查看的网页内容,浏览器再进行解析便可以看到了。
反之,如果传给服务器的Cookie是无效的,或者 Session已经过期了,客户端将不能继续访问页面,此时可能会收到错误的响应或者跳转到登录页面重新登录
Cookie和Session需要配合,一个在客户端,一个在服务端,二者共同协作,就实现了登录控制

常见误区

在谈论 Session 机制的时候,常会听到一种误解-只要关闭浏览器,Session 就消失了。可以想象一下生活中的会员卡,除非顾客主动对店家提出销卡,否则店家是绝对不会轻易删除顾客资料的。对Session来说,也一样,除非程序通知服务器删除一个Session,否则服务器会一直保留。例如:程序一般都是在我们做注销操作时才删除Session。
但是当我们关闭浏览器时,浏览器不会主动在关闭之前通知服务器自己将要被关闭,所以服务器压根不会有机会知道浏览器已经关闭。之所以会产生上面的误解,是因为大部分网站使用会话 Cookie来保存SessionID信息,而浏览器关闭后Cookie就消失了,等浏览器再次连接服务器时,也就无法找到原来的 Session了。如果把服务器设置的 Cookie 保存到硬盘上,或者使用某种手段改写浏览器发出的 HTTP 请求头,把原来的 Cookie 发送给服务器,那么再次打开浏览器时,仍然能够找到原来的SessionID,依旧保持登录状态
而且恰恰是由于关闭浏览器不会导致Session 被删除,因此需要服务器为Session设置一个失效时间,当距离客户端上一次使用Session 的时间超过这个失效时间时,服务器才可以认为客户端已经停止了活动,并删除掉 Session 以节省存储空间。

requests 中使用 session

使用 session 步骤
    1. 先初始化一个session对象,s = requests.Session()
    2. 然后使用这个session对象来进行访问,r = s.post(url,data = user)

在以上的请求中,每次请求其实都相当于发起了一个新的请求。也就是相当于我们每个请求都用了不同的浏览器单独打开的效果。也就是它并不是指的一个会话,即使请求的是同一个网址。

比如

import requests

requests.get('http://httpbin.org/cookies/set/sessioncookie/123456789')
r = requests.get("http://httpbin.org/cookies")
print(r.text)
结果是:
{
  "cookies": {}
}

很明显,这不在一个会话中,无法获取 cookies,那么在一些站点中,我们需要保持一个持久的会话怎么办呢?就像用一个浏览器逛淘宝一样,在不同的选项卡之间跳转,这样其实就是建立了一个长久会话。

解决方案如下

import requests

s = requests.Session()
s.get('http://httpbin.org/cookies/set/sessioncookie/123456789')
r = s.get("http://httpbin.org/cookies")
print(r.text)

在这里我们请求了两次,一次是设置 cookies,一次是获得 cookies。运行结果

{
  "cookies": {
    "sessioncookie": "123456789"
  }
}

发现可以成功获取到 cookies 了,这就是建立一个会话到作用。那么既然会话是一个全局的变量,那么我们肯定可以用来全局的配置了。

import requests

s = requests.Session()
s.headers.update({'x-test': 'true'})
r = s.get('http://httpbin.org/headers', headers={'x-test2': 'true'})
print r.text

通过 s.headers.update 方法设置了 headers 的变量。然后我们又在请求中设置了一个 headers,那么会出现什么结果?

很简单,两个变量都传送过去了。运行结果:

{
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Host": "httpbin.org", 
    "User-Agent": "python-requests/2.9.1", 
    "X-Test": "true", 
    "X-Test2": "true"
  }
}

如果get方法传的headers 同样也是 x-test 呢?

r = s.get('http://httpbin.org/headers', headers={'x-test': 'true'})

嗯,它会覆盖掉全局的配置

{
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Host": "httpbin.org", 
    "User-Agent": "python-requests/2.9.1", 
    "X-Test": "true"
  }
}

那如果不想要全局配置中的一个变量了呢?很简单,设置为 None 即可

r = s.get('http://httpbin.org/headers', headers={'x-test': None})

运行结果

{
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Host": "httpbin.org", 
    "User-Agent": "python-requests/2.9.1"
  }
}

以上就是 session 会话的基本用法

使用Session()对象的写法(Prepared Requests):

#-*- coding:utf-8 -*-
import requests
s = requests.Session()
url1 = 'http://www.exanple.com/login'#登陆地址
url2 = "http://www.example.com/main"#需要登陆才能访问的地址
data={"user":"user","password":"pass"}
headers = { "Accept":"text/html,application/xhtml+xml,application/xml;",
            "Accept-Encoding":"gzip",
            "Accept-Language":"zh-CN,zh;q=0.8",
            "Referer":"http://www.example.com/",
            "User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36"
            }

prepped1 = requests.Request('POST', url1,
    data=data,
    headers=headers
).prepare()
s.send(prepped1)


'''
也可以这样写
res = requests.Request('POST', url1,
data=data,
headers=headers
)
prepared = s.prepare_request(res)
# do something with prepped.body
# do something with prepped.headers
s.send(prepared)
'''

prepare2 = requests.Request('POST', url2,
    headers=headers
).prepare()
res2 = s.send(prepare2)

print res2.content

另一种写法 :

#-*- coding:utf-8 -*-
import requests
s = requests.Session()
url1 = 'http://www.exanple.com/login'#登陆地址
url2 = "http://www.example.com/main"#需要登陆才能访问的页面地址
data={"user":"user","password":"pass"}
headers = { "Accept":"text/html,application/xhtml+xml,application/xml;",
            "Accept-Encoding":"gzip",
            "Accept-Language":"zh-CN,zh;q=0.8",
            "Referer":"http://www.example.com/",
            "User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36"
            }
res1 = s.post(url1, data=data)
res2 = s.post(url2)
print(resp2.content)

SSL证书验证

现在随处可见 https 开头的网站,Requests可以为HTTPS请求验证SSL证书,就像web浏览器一样。要想检查某个主机的SSL证书,你可以使用 verify 参数。
现在 12306 证书不是无效的嘛,来测试一下

import requests

r = requests.get('https://kyfw.12306.cn/otn/', verify=True)
print r.text
结果
requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:590)
果真如此

来试下 github 的

import requests

r = requests.get('https://github.com', verify=True)
print r.text

正常请求,内容我就不输出了。如果我们想跳过刚才 12306 的证书验证,把 verify 设置为 False 即可

import requests

r = requests.get('https://kyfw.12306.cn/otn/', verify=False)
print r.text

发现就可以正常请求了。在默认情况下 verify 是 True,所以如果需要的话,需要手动设置下这个变量。

身份验证

基本身份认证(HTTP Basic Auth):

import requests
from requests.auth import HTTPBasicAuth
 
r = requests.get('https://httpbin.org/hidden-basic-auth/user/passwd', auth=HTTPBasicAuth('user', 'passwd'))
# r = requests.get('https://httpbin.org/hidden-basic-auth/user/passwd', auth=('user', 'passwd'))    # 简写
print(r.json())

另一种非常流行的HTTP身份认证形式是摘要式身份认证,Requests对它的支持也是开箱即可用的:

requests.get(URL, auth=HTTPDigestAuth('user', 'pass'))

requests 设置 http、socks 代理

通过使用 proxies 参数,可以为任意请求配置代理

import requests

proxies = {
  "https": "http://41.118.132.69:4433"
}
r = requests.post("http://httpbin.org/post", proxies=proxies)
print r.text

也可以通过环境变量 HTTP_PROXY 和 HTTPS_PROXY 来配置代理

export HTTP_PROXY="http://10.10.1.10:3128"
export HTTPS_PROXY="http://10.10.1.10:1080"

采集时为避免被封IP,经常会使用代理。requests也有相应的proxies属性。

import requests

proxies = {
"http": "http://10.10.1.10:3128",
"https": "http://10.10.1.10:1080",
}

requests.get("http://www.zhidaow.com", proxies=proxies)

如果代理需要账户和密码,则需这样:

proxies = {
    "http": "http://user:pass@10.10.1.10:3128/",
}

示例:

import requests
 
proxy = '127.0.0.1:10809'
proxies = {
    'http': 'http://' + proxy,
    'https': 'https://' + proxy,
}
try:
    response = requests.get('http://httpbin.org/get', proxies=proxies)
    print(response.text)
except requests.exceptions.ConnectionError as e:
    print('Error', e.args)

如果需要使用 SOCKS5 代理,则首先需要安装一个 Socks 模块:pip3 install "requests[socks]"

同样使用本机运行代理软件的方式,则爬虫设置代理的代码如下:

import requests
 
proxy = '127.0.0.1:10808'
proxies = {
    'http': 'socks5://' + proxy,
    'https': 'socks5://' + proxy
}
try:
    response = requests.get('http://httpbin.org/get', proxies=proxies)
    print(response.text)
except requests.exceptions.ConnectionError as e:
    print('Error', e.args)

还有一种使用 socks 模块进行全局设置的方法,如下:

import requests
import socks
import socket
 
socks.set_default_proxy(socks.SOCKS5, '127.0.0.1', 10808)
socket.socket = socks.socksocket
try:
    response = requests.get('http://httpbin.org/get')
    print(response.text)
except requests.exceptions.ConnectionError as e:
    print('Error', e.args)

如果代理提供了协议,就使用对应协议的代理;如果代理没有协议的话,就在代理上加上http协议。

对于 Chrome 来说,用 Selenium 设置代理的方法也非常简单,设置方法如下:

from selenium import webdriver
 
proxy = '127.0.0.1:10809'
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--proxy-server=http://' + proxy)
browser = webdriver.Chrome(chrome_options=chrome_options)
browser.get('http://httpbin.org/get')

socks5 代理

或者 (可尝试把这代码加到最前面,改成对应的端口)

import socks
import socket

# # set proxy
socks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 10808)
socket.socket = socks.socksocket

Requests POST 多部分编码 (Multipart-Encoded) 的文件方法

http://lovesoo.org/requests-post-multiple-part-encoding-multipart-encoded-file-format.html

更多请参考:

1. 快速上手 — Requests 2.18.1 文档

2. Uploading Data — requests_toolbelt 0.10.1 documentation

Requests本身虽然提供了简单的方法POST多部分编码(Multipart-Encoded)的文件,但是Requests是先读取文件到内存中,然后再构造请求发送出去。
如果需要发送一个非常大的文件作为 multipart/form-data 请求时,为了避免把大文件读取到内存中,我们就希望将请求做成数据流。
默认requests是不支持的(或很困难), 这时需要用到第三方包requests-toolbelt。
两个库POST多部分编码(Multipart-Encoded)的文件示例代码分别如下:

Requests库(先读取文件至内存中)

import requests
 
url = 'http://httpbin.org/post'
files = {'file': open('report.xls', 'rb')}
 
r = requests.post(url, files=files)
print r.text

requests + requests-toolbelt 库(直接发送数据流)

# -*- coding:utf-8 -*-

import requests
from requests_toolbelt.multipart.encoder import MultipartEncoder

proxies = {
    "http": "http://172.17.18.80:8080",
    "https": "http://172.17.18.80:8080",
}

if __name__ == "__main__":
    print "main"

    m = MultipartEncoder(
        fields={'field0': 'value', 'field1': 'value',
                'field2': ('names.txt', open(r'd:\names.txt', 'r'), 'application/zip')}
    )

    r = requests.post('http://httpbin.org/post',
                      data=m,
                      headers={'Content-Type': m.content_type},
                      proxies=proxies)
    print r.text

模拟登录

github 上一个关于模拟登录的项目https://github.com/xchaoinfo/fuck-login

模拟登陆的重点,在于找到表单真实的提交地址,然后携带 cookie,然后 post 数据即可,只要登陆成功,就可以访问其他任意网页,从而获取网页内容。

一个请求,只要正确模拟了method,url,header,body 这四要素,任何内容都能抓下来,而所有的四个要素,只要打开浏览器-审查元素-Network就能看到!

2、httpx 的使用

pypi :https://pypi.org/project/httpx/

安装:pip install httpx

httpx 是 Python3 的一个功能齐全的 HTTP 客户端库。它包括一个集成的命令行客户端,支持HTTP/1.1 和 HTTP/2,并提供 同步和异步 api。HTTPX 目标是与 requests 库的 API 广泛兼容

HTTPX 介绍

示例:

import httpx

r = httpx.get('https://www.example.org/')
print(r.status_code)
print(r.headers['content-type'])
print(r.text)

命令行:pip install 'httpx[cli]'

示例:

import httpx
from PIL import Image
from io import BytesIO


r = httpx.get('https://httpbin.org/get')
r = httpx.post('https://httpbin.org/post', data={'key': 'value'})
r = httpx.put('https://httpbin.org/put', data={'key': 'value'})
r = httpx.delete('https://httpbin.org/delete')
r = httpx.head('https://httpbin.org/get')
r = httpx.options('https://httpbin.org/get')

headers = {'user-agent': 'my-app/0.0.1'}
params = {'key1': 'value1', 'key2': 'value2'}
r = httpx.get('https://httpbin.org/get', params=params, headers=headers)
print(r.text)
print(r.content)
print(r.json())
print(r.encoding)
r.encoding = 'utf-8'

# 从二进制流中创建图片
i = Image.open(BytesIO(r.content))

# 上传文件
files = {'upload-file': open('report.xls', 'rb')}
r = httpx.post("https://httpbin.org/post", files=files)
print(r.text)
# 也可以以原则的形式,显式地设置文件名和内容类型
files = {'upload-file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel')}
r = httpx.post("https://httpbin.org/post", files=files)

快速开始

高级用法

client 实例

如果用过 requests 库,那么 httpx.Client() 就相当于 requests.Session()

为什么要使用客户端?

  • 如果在快速入门中使用顶级 API 发出请求时,HTTPX 必须为每个请求建立新连接(不会重复使用连接)。随着对主机的请求数量的增加,这很快就会变得低效。
  • "Client实例" 使用 HTTP 连接池。这意味着,当您向同一主机发出多个请求时,将重用基础 TCP 连接,而不是为每个请求重新创建一个。

与使用顶级 API 相比,这可以带来显著的性能改进,包括:

  • 减少了跨请求的延迟(无握手)。
  • 减少了 CPU 使用率和往返。
  • 减少网络拥塞。

额外功能

Client实例还支持顶级 API 中不可用的功能,例如:

  • 跨请求的 Cookie 持久性。
  • 对所有传出请求应用配置。
  • 通过 HTTP 代理发送请求。
  • 使用 HTTP/2

用法:( 推荐使用 with 方式 )

with httpx.Client() as client:
    ...

或者,手动显式关闭连接池:
client = httpx.Client()
try:
    ...
finally:
    client.close()

创建完 client 后,就可以使用 client.get、client .post 等方法来发送请求,参数和httpx.get(),httpx.post() 等都相同

跨请求共享配置

通过给 client 构造函数传递参数,可以在所有传出的请求间共享配置。

class Client(BaseClient):
    """
    An HTTP client, with connection pooling, HTTP/2, redirects, cookie persistence, etc.

    It can be shared between threads.

    Usage:

    ```python
    >>> client = httpx.Client()
    >>> response = client.get('https://example.org')
    ```

    **Parameters:**

    * **auth** - *(optional)* An authentication class to use when sending
    requests.
    * **params** - *(optional)* Query parameters to include in request URLs, as
    a string, dictionary, or sequence of two-tuples.
    * **headers** - *(optional)* Dictionary of HTTP headers to include when
    sending requests.
    * **cookies** - *(optional)* Dictionary of Cookie items to include when
    sending requests.
    * **verify** - *(optional)* SSL certificates (a.k.a CA bundle) used to
    verify the identity of requested hosts. Either `True` (default CA bundle),
    a path to an SSL certificate file, an `ssl.SSLContext`, or `False`
    (which will disable verification).
    * **cert** - *(optional)* An SSL certificate used by the requested host
    to authenticate the client. Either a path to an SSL certificate file, or
    two-tuple of (certificate file, key file), or a three-tuple of (certificate
    file, key file, password).
    * **proxies** - *(optional)* A dictionary mapping proxy keys to proxy
    URLs.
    * **timeout** - *(optional)* The timeout configuration to use when sending
    requests.
    * **limits** - *(optional)* The limits configuration to use.
    * **max_redirects** - *(optional)* The maximum number of redirect responses
    that should be followed.
    * **base_url** - *(optional)* A URL to use as the base when building
    request URLs.
    * **transport** - *(optional)* A transport class to use for sending requests
    over the network.
    * **app** - *(optional)* An WSGI application to send requests to,
    rather than sending actual network requests.
    * **trust_env** - *(optional)* Enables or disables usage of environment
    variables for configuration.
    * **default_encoding** - *(optional)* The default encoding to use for decoding
    response text, if no charset information is included in a response Content-Type
    header. Set to a callable for automatic character set detection. Default: "utf-8".
    """

    def __init__(
        self,
        *,
        auth: typing.Optional[AuthTypes] = None,
        params: typing.Optional[QueryParamTypes] = None,
        headers: typing.Optional[HeaderTypes] = None,
        cookies: typing.Optional[CookieTypes] = None,
        verify: VerifyTypes = True,
        cert: typing.Optional[CertTypes] = None,
        http1: bool = True,
        http2: bool = False,
        proxies: typing.Optional[ProxiesTypes] = None,
        mounts: typing.Optional[typing.Mapping[str, BaseTransport]] = None,
        timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
        follow_redirects: bool = False,
        limits: Limits = DEFAULT_LIMITS,
        max_redirects: int = DEFAULT_MAX_REDIRECTS,
        event_hooks: typing.Optional[
            typing.Mapping[str, typing.List[EventHook]]
        ] = None,
        base_url: URLTypes = "",
        transport: typing.Optional[BaseTransport] = None,
        app: typing.Optional[typing.Callable[..., typing.Any]] = None,
        trust_env: bool = True,
        default_encoding: typing.Union[str, typing.Callable[[bytes], str]] = "utf-8",
    ):

夸请求 共享配置示例:共享请求头

>>> url = 'http://httpbin.org/headers'
>>> headers = {'user-agent': 'my-app/0.0.1'}
>>> with httpx.Client(headers=headers) as client:
...     r = client.get(url)
...
>>> r.json()['headers']['User-Agent']

配置 合并

在 "client级别" 和 "请求级别" 同时提供配置选项时,可能会发生以下两种情况之一:

  • 对于 headers、query parameters、cookies,这些值将组合在一起。例如:

>>> headers = {'X-Auth': 'from-client'}
>>> params = {'client_id': 'client1'}
>>> with httpx.Client(headers=headers, params=params) as client:
...     headers = {'X-Custom': 'from-request'}
...     params = {'request_id': 'request1'}
...     r = client.get('https://example.com', headers=headers, params=params)
...
>>> r.request.url
URL('https://example.com?client_id=client1&request_id=request1')
>>> r.request.headers['X-Auth']
'from-client'
>>> r.request.headers['X-Custom']
'from-request'

  • 对于所有其他参数,请求级别值优先。例如:

>>> with httpx.Client(auth=('tom', 'mot123')) as client:
...     r = client.get('https://example.com', auth=('alice', 'ecila123'))
...
>>> _, _, auth = r.request.headers['Authorization'].partition(' ')
>>> import base64
>>> base64.b64decode(auth)
b'alice:ecila123'

client 才有的配置

例如,允许您在所有传出请求前面附加一个 URL:base_url

>>> with httpx.Client(base_url='http://httpbin.org') as client:
...     r = client.get('/headers')
...
>>> r.request.url
URL('http://httpbin.org/headers')

有关所有可用客户端参数的列表,请参阅 client API 参考。

字符集编码、自动检测

import httpx
import chardet

def autodetect(content):
    return chardet.detect(content).get("encoding")

client = httpx.Client(default_encoding=autodetect)
response = client.get(...)
print(response.encoding)  
print(response.text)

调用 Python Web 应用程序

可以将 httpx client 配置为使用 WSGI 协议直接调用 Python Web 应用程序。

这对于两个主要用例特别有用:

  • 在测试用例中用作客户端。
  • 在测试期间或在开发/过渡环境中模拟外部服务。

示例:可以打上断点,查看执行流程

from flask import Flask
import httpx

app = Flask(__name__)


@app.route("/")
def hello():
    return "Hello World!"


with httpx.Client(app=app, base_url="http://testserver") as client:
    r = client.get("/")
    assert r.status_code == 200
    assert r.text == "Hello World!"

request 实例

为了最大限度地控制通过网络发送的内容,HTTPX 支持构建显式 request 实例:

request = httpx.Request("GET", "https://example.com")

将 实例 发送到网络:Request.send()

with httpx.Client() as client:
    response = client.send(request)
    ...

默认的 "参数合并" 不支持在一个方法中同时使用 client-level 和 request-level 的参数,如果想要使用,则可以使用 .build_request() 构造一个实例,使用构造的实例去用

headers = {"X-Api-Key": "...", "X-Client-ID": "ABC123"}

with httpx.Client(headers=headers) as client:
    request = client.build_request("GET", "https://api.example.com")

    print(request.headers["X-Client-ID"])  # "ABC123"

    # Don't send the API key for this particular request.
    del request.headers["X-Api-Key"]

    response = client.send(request)
    ...

事件钩子

HTTPX 允许向 client 注册“事件钩子”,这些钩子在每次发生特定类型的事件时被激活并调用

目前有两个事件挂钩:

  • request- 在请求完全准备好之后,但在发送到网络之前调用。已传递实例。request
  • response- 在从网络获取响应之后,但在将其返回给调用方之前调用。已传递实例。response

这些允许您安装客户端范围的功能,例如日志记录、监视或跟踪。

import httpx


def log_request(request):
    print(f"Request event hook: {request.method} {request.url} - Waiting for response")


def log_response(response):
    request = response.request
    print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}")


client = httpx.Client(event_hooks={'request': [log_request], 'response': [log_response]})

检查和修改 已安装的钩子

client = httpx.Client() client.event_hooks['request'] = [log_request] client.event_hooks['response'] = [log_response, raise_on_4xx_5xx]

监控下载进度

如果需要监视大型响应的下载进度,可以使用响应流式处理并检查属性。response.num_bytes_downloaded

示例:

import tempfile

import httpx
from tqdm import tqdm

with tempfile.NamedTemporaryFile() as download_file:
    url = "https://speed.hetzner.de/100MB.bin"
    with httpx.stream("GET", url) as response:
        total = int(response.headers["Content-Length"])

        with tqdm(total=total, unit_scale=True, unit_divisor=1024, unit="B") as progress:
            num_bytes_downloaded = response.num_bytes_downloaded
            for chunk in response.iter_bytes():
                download_file.write(chunk)
                progress.update(response.num_bytes_downloaded - num_bytes_downloaded)
                num_bytes_downloaded = response.num_bytes_downloaded
pass

示例:

import tempfile
import httpx
import rich.progress

with tempfile.NamedTemporaryFile() as download_file:
    url = "https://speed.hetzner.de/100MB.bin"
    with httpx.stream("GET", url) as response:
        total = int(response.headers["Content-Length"])

        with rich.progress.Progress(
            "[progress.percentage]{task.percentage:>3.0f}%",
            rich.progress.BarColumn(bar_width=None),
            rich.progress.DownloadColumn(),
            rich.progress.TransferSpeedColumn(),
        ) as progress:
            download_task = progress.add_task("Download", total=total)
            for chunk in response.iter_bytes():
                download_file.write(chunk)
                progress.update(download_task, completed=response.num_bytes_downloaded)

监控上传进度

import io
import random

import httpx
from tqdm import tqdm


def gen():
    """
    this is a complete example with generated random bytes.
    you can replace `io.BytesIO` with real file object.
    """
    total = 32 * 1024 * 1024  # 32m
    with tqdm(ascii=True, unit_scale=True, unit='B', unit_divisor=1024, total=total) as bar:
        with io.BytesIO(random.randbytes(total)) as f:
            while data := f.read(1024):
                yield data
                bar.update(len(data))


httpx.post("https://httpbin.org/post", content=gen())

"HTTP、SOCKS" 代理

proxies = {
    "http://": "http://localhost:8030",
    "https://": "http://localhost:8031",
}

HTTPX 支持通过在 client 初始化时传递 proxies 参数。               
        with httpx.Client(proxies=proxies) as client:
            ...
也支持顶级 API 函数传递 proxies 参数。
        httpx.get(..., proxies=...)

HTTPX 提供了细粒度控件,用于决定哪些请求必须通过proxy,哪些请求不能通过proxy。

proxies = {
    "http://": "http://username:password@localhost:8030",
    # ...
}
proxies 字典通过 "键值对"的形式映射 "url 和 proxy "。
HTTPX 将请求的 URL 与 "keys" 进行匹配,以决定应使用哪个 proxy(如果有)。
匹配是从最具体的 "keys" (例如:https://<domain>:<port>) 到最不具体 keys (例如:https://)

HTTPX 支持基于 "scheme、domain、port" 或这些方案的自由组合。

一个复杂 proxy 的配置示例,可以组合上述路由功能来构建复杂的路由配置。例如

SOCKS

超时配置

为单个请求设置超时:

# Using the top-level API:
httpx.get('http://example.com/api/v1/example', timeout=10.0)

# Using a client instance:
with httpx.Client() as client:
    client.get("http://example.com/api/v1/example", timeout=10.0)

或者禁用单个请求的超时:

# Using the top-level API:
httpx.get('http://example.com/api/v1/example', timeout=None)

# Using a client instance:
with httpx.Client() as client:
    client.get("http://example.com/api/v1/example", timeout=None)

client = httpx.Client()              # Use a default 5s timeout everywhere.
client = httpx.Client(timeout=10.0)  # Use a default 10s timeout everywhere.
client = httpx.Client(timeout=None)  # Disable all timeouts by default.

池 限制配置

可以在 client 上使用 limits 关键字参数控制连接池大小。

  • max_keepalive_connections、允许的保持活动连接数或始终 允许。(默认值 20)None
  • max_connections、允许的最大连接数或无限制。 (默认 100)None
  • keepalive_expiry、空闲保持活动连接的时间限制(以秒为单位)或无限制。(默认 5)None

limits = httpx.Limits(max_keepalive_connections=5, max_connections=10)
client = httpx.Client(limits=limits)

多部分文件编码

>>> files = {'upload-file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel')}
>>> r = httpx.post("https://httpbin.org/post", files=files)
>>> print(r.text)
{
  ...
  "files": {
    "upload-file": "<... binary content ...>"
  },
  ...
}

更具体地说,如果将元组用作值,它必须具有 2 到 3 个元素:

  • 第一个元素是可选文件名,可以设置为 。None
  • 第二个元素可以是类似文件的对象或字符串,它们将自动 以 UTF-8 编码。
  • 可选的第三个元素可用于指定要上传的文件的 MIME 类型。如果未指定,HTTPX 将尝试猜测基于 MIME 类型 在文件名上,未知文件扩展名默认为“应用程序/八位字节流”。 如果文件名显式设置为 则 HTTPX 将不包含内容类型 MIME 标头字段。None

>>> files = {'upload-file': (None, 'text content', 'text/plain')}
>>> r = httpx.post("https://httpbin.org/post", files=files)
>>> print(r.text)
{
  ...
  "files": {},
  "form": {
    "upload-file": "text-content"
  },
  ...
}

自定义身份验证

SSL 证书

使用自定义 CA ,使用 verify 参数。

import httpx

r = httpx.get("https://example.org", verify="path/to/client.pem")

或者,可以传递一个 标准库 .ssl.SSLContext

>>> import ssl
>>> import httpx
>>> context = ssl.create_default_context()
>>> context.load_verify_locations(cafile="/tmp/client.pem")
>>> httpx.get('https://example.org', verify=context)
<Response [200 OK]>

禁用SSL

import httpx
r = httpx.get("https://example.org", verify=False)

自定义 传输

HTTPX 的 Client 也接收一个 transport 参数,This argument allows you to provide a custom Transport object that will be used to perform the actual sending of the requests.

用法:对于某些高级配置,您可能需要实例化传输 类,并将其传递给客户端实例。一个例子是只能通过这个低级 API 提供 local_address 的配置。

>>> import httpx
>>> transport = httpx.HTTPTransport(local_address="0.0.0.0")
>>> client = httpx.Client(transport=transport)

也可以通过此接口进行连接重试

>>> import httpx
>>> transport = httpx.HTTPTransport(retries=1)
>>> client = httpx.Client(transport=transport)

实例化传输时,只能通过低级 API 提供的 Unix 域套接字进行连接 这一种方式。

>>> import httpx
>>> # Connect to the Docker API via a Unix Socket.
>>> transport = httpx.HTTPTransport(uds="/var/run/docker.sock")
>>> client = httpx.Client(transport=transport)
>>> response = client.get("http://docker/info")
>>> response.json()
{"ID": "...", "Containers": 4, "Images": 74, ...}

使用 urllib3 进行传输

>>> import httpx
>>> from urllib3_transport import URLLib3Transport
>>> client = httpx.Client(transport=URLLib3Transport())
>>> client.get("https://example.org")
<Response [200 OK]>

编写自定义传输

传输实例必须实现低级传输 API,该 API 处理 发送单个请求并返回响应。你要么是 httpx.BaseTransport子类,要么是 httpx.AsyncBaseTransport子类。

在传输 API 层,我们使用熟悉的 Request 和 Response 模型

有关更多详细信息,请参阅 handle_request 和 handle_async_request 文档字符串 关于传输 API 的细节。

自定义传输实现的完整示例如下:

import json
import httpx


class HelloWorldTransport(httpx.BaseTransport):
    """
    A mock transport that always returns a JSON "Hello, world!" response.
    """

    def handle_request(self, request):
        message = {"text": "Hello, world!"}
        content = json.dumps(message).encode("utf-8")
        stream = httpx.ByteStream(content)
        headers = [(b"content-type", b"application/json")]
        return httpx.Response(200, headers=headers, stream=stream)

以相同的方式使用:

>>> import httpx
>>> client = httpx.Client(transport=HelloWorldTransport())
>>> response = client.get("https://example.org/")
>>> response.json()
{"text": "Hello, world!"}

模拟 传输

在测试过程中,能够模拟传输通常很有用, 并返回预先确定的响应,而不是发出实际的网络请求。httpx.MockTransport 类接受可以使用的处理程序函数,要将请求映射到预先确定的响应。

def handler(request):
    return httpx.Response(200, json={"text": "Hello, world!"})


# Switch to a mock transport, if the TESTING environment variable is set.
if os.environ.get('TESTING', '').upper() == "TRUE":
    transport = httpx.MockTransport(handler)
else:
    transport = httpx.HTTPTransport()

client = httpx.Client(transport=transport)

对于更高级的用例,您可能需要查看第三方 mocking library、RESPX 或 pytest-httpx library

安装运输

还可以针对给定的方案或域挂载传输,以控制 传出请求应通过具有相同样式的传输进行路由 用于指定代理路由

import httpx

class HTTPSRedirectTransport(httpx.BaseTransport):
    """
    A transport that always redirects to HTTPS.
    """

    def handle_request(self, method, url, headers, stream, extensions):
        scheme, host, port, path = url
        if port is None:
            location = b"https://%s%s" % (host, path)
        else:
            location = b"https://%s:%d%s" % (host, port, path)
        stream = httpx.ByteStream(b"")
        headers = [(b"location", location)]
        extensions = {}
        return 303, headers, stream, extensions


# A client where any `http` requests are always redirected to `https`
mounts = {'http://': HTTPSRedirectTransport()}
client = httpx.Client(mounts=mounts)

关于如何利用安装运输工具的其他一些草图......

在单个给定域上禁用 HTTP/2...

mounts = {
    "all://": httpx.HTTPTransport(http2=True),
    "all://*example.org": httpx.HTTPTransport()
}
client = httpx.Client(mounts=mounts)

模拟对给定域的请求:

# All requests to "example.org" should be mocked out.
# Other requests occur as usual.
def handler(request):
    return httpx.Response(200, json={"text": "Hello, World!"})

mounts = {"all://example.org": httpx.MockTransport(handler)}
client = httpx.Client(mounts=mounts)

添加对自定义方案的支持:

# Support URLs like "file:///Users/sylvia_green/websites/new_client/index.html"
mounts = {"file://": FileSystemTransport()}
client = httpx.Client(mounts=mounts)

使用 指南

异步

协程,英文叫作 coroutine,又称微线程、纤程,是一种运行在用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程在调度切换时,将寄存器上下文和栈保存到其他地方,等切回来的时候。再恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态,即所有局部状态的一个特定组合,每次过程重人,就相当于进人上一次调用的状态
协程本质上是个单进程,相对于多进程来说,它没有线程上下文切换的开销,没有原子操作锁定及同步的开销,编程模型也非常简单。
可以使用协程来实现异步操作,例如在网络爬虫场景下,我们发出一个请求之后,需要等待一定时间才能得到响应,但其实在这个等待过程中,程序可以干许多其他事情,等得到响应之后再切换回来继续处理,这样可以充分利用CPU和其他资源,这就是协程的优势

发出异步请求,需要使用 .AsyncClient

>>> async with httpx.AsyncClient() as client:
...     r = await client.get('https://www.example.com/')
...
>>> r
<Response [200 OK]>

使用格式 :response = await client.get(...)

  • AsyncClient.get(url, ...)
  • AsyncClient.options(url, ...)
  • AsyncClient.head(url, ...)
  • AsyncClient.post(url, ...)
  • AsyncClient.put(url, ...)
  • AsyncClient.patch(url, ...)
  • AsyncClient.delete(url, ...)
  • AsyncClient.request(method, url, ...)
  • AsyncClient.send(request, ...)

异步上下文

注意:为了从连接池中获得最大好处,请不要实例化多个 client 实例 。 例如,通过使用 async with 可以创建只有一个单个作用域的 client 实例,在该 client 实例中传递任何需要的参数。

async with httpx.AsyncClient() as client:
    ...

或者,显式关闭客户端:await client.aclose()

client = httpx.AsyncClient()
...
await client.aclose()

示例:

import asyncio
import httpx
import datetime

pool_size_limit = httpx.Limits(max_keepalive_connections=300, max_connections=500)


async def fetch(url=None):
    async with httpx.AsyncClient(limits=pool_size_limit) as client:
        resp = await client.get('https://www.example.com/')
        print(resp.status_code)


async def main():
    url = 'https://www.httpbin.org/delay/5'
    task_list = []
    for index in range(100):
        task_list.append(asyncio.create_task(fetch(url)))
    await asyncio.wait(task_list)


if __name__ == '__main__':
    time_1 = datetime.datetime.now()
    asyncio.run(main())
    time_2 = datetime.datetime.now()
    print((time_2 - time_1).seconds)

流式 处理 响应

该方法是一个异步上下文块。AsyncClient.stream(method, url, ...)

>>> client = httpx.AsyncClient()
>>> async with client.stream('GET', 'https://www.example.com/') as response:
...     async for chunk in response.aiter_bytes():
...      

异步响应流式处理方法包括:

  • Response.aread()- 用于有条件地读取流块内的响应。
  • Response.aiter_bytes()- 用于将响应内容流式传输为字节。
  • Response.aiter_text()- 用于将响应内容流式传输为文本。
  • Response.aiter_lines()- 用于将响应内容流式传输为文本行。
  • Response.aiter_raw()- 用于流式传输原始响应字节,而无需应用内容解码。
  • Response.aclose()- 用于关闭响应。通常不需要它,因为 stream block 会在退出时自动关闭响应。

对于不能使用上下文块进行处理的情况,可以通过使用 发送请求实例来进入“手动模式”。client.send(..., stream=True)

使用 Starlette 将响应转发到流式处理 Web 终结点的上下文中的示例:

import httpx
from starlette.background import BackgroundTask
from starlette.responses import StreamingResponse

client = httpx.AsyncClient()

async def home(request):
    req = client.build_request("GET", "https://www.example.com/")
    r = await client.send(req, stream=True)
    return StreamingResponse(r.aiter_text(), background=BackgroundTask(r.aclose))

流式 处理 请求

async def upload_bytes():
    ...  # yield byte content

await client.post(url, content=upload_bytes())

显式 传输 实例

直接实例化传输实例时,需要使用 .httpx.AsyncHTTPTransport

>>> import httpx
>>> transport = httpx.AsyncHTTPTransport(retries=1)
>>> async with httpx.AsyncClient(transport=transport) as client:
>>>     ...

支持的异步环境

HTTPX 支持 asyncio 或 trio 作为异步环境。它将自动检测这两者中的哪一个用作后端 用于套接字操作和并发基元。

AsyncIO是Python的内置库,用于编写具有async/await语法的并发代码。

import asyncio
import httpx

async def main():
    async with httpx.AsyncClient() as client:
        response = await client.get('https://www.example.com/')
        print(response)

asyncio.run(main())

Trio 是一个替代的异步库, 围绕结构化并发原则设计。

import httpx
import trio

async def main():
    async with httpx.AsyncClient() as client:
        response = await client.get('https://www.example.com/')
        print(response)

trio.run(main)

AnyIO ( 地址:https://github.com/agronholm/anyio )。AnyIO 是一个异步网络和并发库,它工作在 asyncio 或 trio 之上。它与所选后端的本机库混合在一起(默认为 asyncio)。

import httpx
import anyio

async def main():
    async with httpx.AsyncClient() as client:
        response = await client.get('https://www.example.com/')
        print(response)

anyio.run(main, backend='trio')

调用 Python Web 应用程序

就像httpx.Client允许您直接调用WSGI Web应用程序一样, httpx.AsyncClient允许您直接调用 ASGI Web 应用程序。

以 Starlette 应用程序为例:

from starlette.applications import Starlette
from starlette.responses import HTMLResponse
from starlette.routing import Route


async def hello(request):
    return HTMLResponse("Hello World!")


app = Starlette(routes=[Route("/", hello)])

可以直接针对应用程序发出请求,如下所示:

>>> import httpx
>>> async with httpx.AsyncClient(app=app, base_url="http://testserver") as client:
...     r = await client.get("/")
...     assert r.status_code == 200
...     assert r.text == "Hello World!"

启用 HTTP/2

HTTP / 2是HTTP协议的主要新版本,它提供了更多的 高效运输,具有潜在的性能优势。HTTP/2 不会改变 请求或响应的核心语义,但改变了数据的方式 发送到服务器和从服务器发送。

HTTP/1 不是 HTTP/1.2 使用的文本格式,而是二进制格式。 二进制格式提供完整的请求和响应多路复用,并且高效 压缩 HTTP 标头。流多路复用意味着HTTP / 1.1 每个并发请求需要一个TCP流,HTTP / 2允许单个TCP 流以处理多个并发请求。

HTTP/2 还提供对响应优先级、 和服务器推送。

有关HTTP / 2的综合指南,您可能需要查看“http2解释”。

使用 httpx client 时,默认情况下不启用 HTTP/2 支持,因为 HTTP/1.1 是一个成熟的、久经沙场的传输层,所以现在HTTP/1.1是更好的选择。 未来版本的 httpx 可能会默认启用 HTTP/2 支持.

首先确保安装 可选的 HTTP/2 依赖项:pip install httpx[http2]
然后实例化启用了 HTTP/2 支持的客户端:client = httpx.AsyncClient(http2=True)

async with httpx.AsyncClient(http2=True) as client:
    ...

在客户端上启用 HTTP/2 支持并不一定意味着您的 请求和响应将通过 HTTP/2 传输,因为客户端服务器都需要支持 HTTP/2。如果连接到仅 支持 HTTP/1.1 客户端将使用标准的 HTTP/1.1 连接。

检查 http 版本

client = httpx.AsyncClient(http2=True)
response = await client.get(...)
print(response.http_version)  # "HTTP/1.0", "HTTP/1.1", or "HTTP/2".

API 引用

API 接口

异 常

异常层次结构

  • HTTPError
    • RequestError
      • TransportError
        • TimeoutException
          • ConnectTimeout
          • ReadTimeout
          • WriteTimeout
          • PoolTimeout
        • NetworkError
          • ConnectError
          • ReadError
          • WriteError
          • CloseError
        • ProtocolError
          • LocalProtocolError
          • RemoteProtocolError
        • ProxyError
        • UnsupportedProtocol
      • DecodingError
      • TooManyRedirects
    • HTTPStatusError
  • InvalidURL
  • CookieConflict
  • StreamError
    • StreamConsumed
    • ResponseNotRead
    • RequestNotRead
    • StreamClosed

3、curl_cffi

项目地址:https://github.com/lexiforest/curl_cffi
官方文档:https://curl-cffi.readthedocs.io

curl_cffi 简介

curl_cffi 是一个基于libcurl的Python HTTP客户端库,通过 CFFI(C Foreign Function Interface)技术实现了对curl-impersonate项目的绑定。它最大的特点是能够模拟浏览器的TLS/JA3指纹和HTTP/2协议特征,有效绕过网站的反爬虫机制。curl_cffi 作为新一代HTTP客户端库,在反爬虫对抗、协议兼容性和性能方面表现出色。

对于 pycurl 需要一个启用 http/3 的 libcurl 才能使其工作

特征

  • 支持 JA3/TLS 和 http2 指纹模拟,包括最近的浏览器和自定义指纹。
  • 比 requests / httpx 快得多,与 aiohttp / pycurl 相当,见基准测试
  • 模仿 requests 的 API,无需增加学习成本。
  • 支持对每个请求进行代理轮换。
  • 支持 http 2.0 和 3.0,requests 则不支持。
  • 支持 websocket。
  • 麻省理工学院许可。

核心特性

  • 浏览器指纹模拟。支持预设Chrome、Edge、Safari等主流浏览器的TLS指纹,例如:response = requests.get("https://example.com", impersonate="chrome110")
  • 高性能异步支持。内置异步会话管理,轻松处理高并发请求:
    async with AsyncSession() as session:
        response = await session.get("https://example.com")
  • 协议兼容性。全面支持HTTP/1.1、HTTP/2和HTTP/3协议,突破requests库的协议限制。
  • 低级API接口,提供对libcurl底层参数的直接访问,例如设置超时、代理等:curl_cffi.setopt(curl, CURLOPT_TIMEOUT, 30)

工作原理

curl_cffi 基于 cURL Impersonate,它是一个能够生成与真实浏览器匹配的 TLS 指纹的库。

当你发送 HTTPS 请求时,会发生一次 TLS 握手,从而产生一个独特的 TLS 指纹。由于普通 HTTP 客户端与浏览器存在差异,这些指纹可能会暴露自动化行为,进而触发反爬虫防护。

cURL Impersonate 会对 cURL 进行修改,使其匹配真实的浏览器 TLS 指纹:

  • TLS 库调整:使用浏览器所用的 TLS 库而不是 cURL 本身的库。
  • 配置变更:调整 TLS 扩展和 SSL 选项以模仿浏览器。
  • HTTP/2 定制:匹配浏览器的握手方式。
  • 自定义 cURL 参数:使用 --ciphers--curves 以及自定义头部信息来实现更精准的模拟。

这样的改动使得请求更接近浏览器的行为,从而帮助绕过反爬虫检测。欲了解更多信息,请参阅关于 cURL Impersonate 的指南

TLS 指纹

现在绝大多数的网站都已经使用了 HTTPS,要建立 HTTPS 链接,服务器和客户端之间首先要进行 TLS 握手,在握手过程中交换双方支持的 TLS 版本,加密算法等信息。不同的客户端之间的差异 很大,而且一般这些信息还都是稳定的,所以服务端就可以根据 TLS 的握手信息来作为特征,识别 一个请求是普通的用户浏览器访问,还是来自 Python 脚本等的自动化访问。

JA3 是生成 TLS 指纹的一个常用算法。它的工作原理也很简单,大概就是把以上特征拼接并求 md5。

有证据表明,阿里云、华为云、Akamai 和 Cloudflare 都在使用 TLS 指纹技术来识别机器访问流量。 Akamai 更是直接在宣传稿中说明了在通过 TLS 指纹技术检测非法请求。

查看 tls 指纹的网站有:

不同网站的生成的指纹可能有差异,但是多次访问同一个网站生成的指纹是稳定的,而且能区分开 不同客户端。:https://zhuanlan.zhihu.com/p/601474166

非法指纹黑名单

这个思路很直接,把常用的爬虫工具的指纹收集起来,然后全都屏蔽了就好了。比如说:curl, requests, golang 访问时,直接 403。当然,突破也很简单,别用默认的指纹,直接随便改一下 tls hello 包的值就行了。

比如,修改 httpx 的 TLS 协议。以 httpx 为例:

# 默认 cipher 在这里定义:https://github.com/encode/httpx/blob/master/httpx/_config.py
import ssl
import httpx

# create an ssl context
ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS)
CIPHERS = 'ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:DH+CHACHA20:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:DH+HIGH:RSA+AESGCM:RSA+AES:RSA+HIGH'
ssl_context.set_ciphers(CIPHERS)

r = httpx.get('https://tls.browserleaks.com/json', verify=ssl_context)
print(r.json())

# {'ja3_hash': 'cc8fc04d55d8c9c318409384eee468b6'

可以看到 JA3 指纹已经变了。

合法指纹白名单

既然指纹可以随便改,那就直接只认常用浏览器的指纹好了。这时候如果爬虫或者其他脚本再想要 突破防御,需要把每一个值都改成和浏览器都完全相同,难度还是挺大的。尤其是考虑到大多数 语言的标准库都是直接使用系统的 SSL 库,很多底层的东西直接没提供接口,所以这种防御还是非常 有效的。

例如,Python 使用了 OpenSSL,而 Chrome 则使用了 BoringSSL,这两者的细节差异很多。所以, 纯 Python 的库,比如 requests 和 httpx,再怎么改也不可能改成和 Chrome 一样的指纹,必须 使用第三方的 C 扩展库,才能够实现完美模拟浏览器指纹。

此外,还又一个小细节,可以由 TLS 指纹反推出客户端是从哪些操作系统或者软件来的,如果和 User-Agent 互相矛盾,那也说明有问题。不过实际中,我还没有遇到这种情况。

为了完美模拟浏览器,国外有大佬给 curl 打了一些 patch,把相应组件全部都替换成了浏览器使用 库,连版本都保持一致,这样就得到了和浏览器完全一样的指纹,这个库是:curl-impersonate

Python 中早就有 curl 的 binding -- pycurl,但是非常难用,安装的时候总是出现编译错误;接口 也很低级,相比 requests,甚至 urllib,用起来都比较费劲。

其他指纹技术概览

  1. HTTP Header 指纹。通过浏览器发送的 header 的顺序和值的组合来判断是合法用户还是爬虫
  2. DNS 指纹。参考:http://dnscookie.com
  3. 浏览器指纹。通过 canvas,webgl 等计算得到一个唯一指纹,Cookie 禁用时监视用户的主流技术
  4. TCP 指纹。也是根据 TCP 的一些窗口、拥塞控制等参数嗅探、猜测用户的系统版本

总结一下,指纹技术就是通过不同的设备和客户端在参数上的微妙差异来识别用户。本来按照规范, 这些值都是应该任意选取的,但是,现实世界中,服务端反而对不同值采取了区别对待。指纹技术 可以说应用到了 OSI 网络模型中所有可能的层,基于 HTTP header 顺序的指纹工作在第七层应用层, SSL/TLS 指纹工作在传输层和应用层之间,TCP 指纹在第四层传输层。而在 TCP 之下的 IP 层和物理 层,因为建立的不是端到端的链路,所以只能收集上一跳的指纹,没有任何意义。

对于爬虫来说,User-Agent 相当于自报门户。除了初学者以外,没有人会顶着 Python/3.9 requests 这样的 UA 去爬的,而指纹则是很难更改的内部特征。通过指纹技术可以防御一大批爬虫,而使用 能够模拟指纹的 http client 则轻松突破这道防线。

对于普通用户来说,各种指纹造成了极大的隐私泄露风险。即使按照 GDPR 等监管政策的要求,用户拒绝使用 Cookie 时,互联网公司依然可以通过各种指纹来定位追踪用户,乃至于区别对待。平等、 匿名、自由地使用个人数据和公开数据应该是一项基本人权。在立法赶不上技术更新的时代,我们应该用技术手段捍卫自己的权利。

对于 Cloudflare、5s盾等,只能说 TLS/JA3 指纹只是人家防御的一部份,虽然很重要,但不是全部。建议搜索 cf_clearance 相关开源项目,或者使用专业的商业服务,如 YesCaptcha 等。

安装 curl_cffi

  • 安装:pip install curl_cffi --upgrade
  • 安装测试版:pip install curl_cffi --upgrade --pre
  • 从 GitHub 安装不稳定版本:
    git clone https://github.com/lexiforest/curl_cffi/
    cd curl_cffi
    make preprocess
    pip install .

验证

from curl_cffi import requests
r = requests.get("https://tools.scrapfly.io/api/fp/ja3", impersonate="chrome")
print(r.json())  # 应返回包含JA3指纹信息的JSON
from curl_cffi import requests

# 注意这个 impersonate 参数,指定了模拟哪个浏览器
r = requests.get("https://tls.browserleaks.com/json", impersonate="chrome101")

print(r.json())
# output: {'ja3_hash': '53ff64ddf993ca882b70e1c82af5da49'

使用 curl_cffi

类似 requests

from curl_cffi import requests

r = requests.get("https://tls.browserleaks.com/json", impersonate="chrome")
import curl_cffi

# Notice the impersonate parameter
r = curl_cffi.get("https://tls.browserleaks.com/json", impersonate="chrome")

print(r.json())
# output: {..., "ja3n_hash": "aa56c057ad164ec4fdcb7a5a283be9fc", ...}
# the js3n fingerprint should be the same as target browser

# To keep using the latest browser version as `curl_cffi` updates,
# simply set impersonate="chrome" without specifying a version.
# Other similar values are: "safari" and "safari_ios"
r = curl_cffi.get("https://tls.browserleaks.com/json", impersonate="chrome")

# Randomly choose a browser version based on current market share in real world
# from: https://caniuse.com/usage-table
# NOTE: this is a pro feature.
r = curl_cffi.get("https://example.com", impersonate="realworld")

# To pin a specific version, use version numbers together.
r = curl_cffi.get("https://tls.browserleaks.com/json", impersonate="chrome124")

# To impersonate other than browsers, bring your own ja3/akamai strings
# See examples directory for details.
r = curl_cffi.get("https://tls.browserleaks.com/json", ja3=..., akamai=...)

# http/socks proxies are supported
proxies = {"https": "http://localhost:3128"}
r = curl_cffi.get("https://tls.browserleaks.com/json", impersonate="chrome", proxies=proxies)

proxies = {"https": "socks://localhost:3128"}
r = curl_cffi.get("https://tls.browserleaks.com/json", impersonate="chrome", proxies=proxies)

Sessions  会话

from curl_cffi import requests

s = requests.Session()
s = curl_cffi.Session()

# httpbin is a http test website, this endpoint makes the server set cookies
s.get("https://httpbin.org/cookies/set/foo/bar")
print(s.cookies)
# <Cookies[<Cookie foo=bar for httpbin.org />]>

# retrieve cookies again to verify
r = s.get("https://httpbin.org/cookies")
print(r.json())
# {'cookies': {'foo': 'bar'}}

Asyncio (异步)

from curl_cffi import AsyncSession

async with AsyncSession() as s:
    r = await s.get("https://example.com")

更多并发性

import asyncio
from curl_cffi import AsyncSession

urls = [
    "https://google.com/",
    "https://facebook.com/",
    "https://twitter.com/",
]

async with AsyncSession() as s:
    tasks = []
    for url in urls:
        task = s.get(url)
        tasks.append(task)
    results = await asyncio.gather(*tasks)

发起 GET 请求

from curl_cffi import requests

# 模拟Chrome 110的TLS指纹
response = requests.get(
    "https://httpbin.org/get",
    impersonate="chrome110",
    params={"key": "value"},
    headers={"User-Agent": "Custom Agent"}
)

print(response.status_code)
print(response.text)

发起 POST 请求

# 发送JSON数据
payload = {"name": "John", "age": 30}
response = requests.post(
    "https://httpbin.org/post",
    json=payload,
    impersonate="chrome110"
)

# 发送文件
mp = curl_cffi.CurlMime()
mp.addpart(
    name="file",
    content_type="application/octet-stream",
    filename="test.txt",
    local_path="./test.txt"
)
response = requests.post("https://httpbin.org/post", multipart=mp)

也可以用底层一点的 Curl 对象:

from curl_cffi import Curl, CurlOpt
from io import BytesIO

buffer = BytesIO()
c = Curl()
c.setopt(CurlOpt.URL, b'https://tls.browserleaks.com/json')
c.setopt(CurlOpt.WRITEDATA, buffer)

c.impersonate("chrome101")

c.perform()
c.close()
body = buffer.getvalue()
print(body.decode())

代理 配置

proxies = {
    "http": "http://localhost:3128",
    "https": "socks5h://localhost:9050"
}

response = requests.get(
    "https://example.com",
    proxies=proxies,
    impersonate="chrome110"
)

会话 管理

with curl_cffi.Session() as session:
    # 自动保存cookies
    session.get("https://httpbin.org/cookies/set/sessionid/123")
    response = session.get("https://httpbin.org/cookies")
    print(response.json())

WebSocket 支持

def on_message(ws, message):
    print(f"Received: {message}")

with curl_cffi.Session() as session:
    ws = session.ws_connect(
        "wss://echo.websocket.org",
        on_message=on_message
    )
    ws.send("Hello, WebSocket!")
    ws.run_forever()
from curl_cffi import WebSocket

def on_message(ws: WebSocket, message: str | bytes):
    print(message)

ws = WebSocket(on_message=on_message)
ws.run_forever("wss://api.gemini.com/v1/marketdata/BTCUSD")

Asyncio WebSockets

import asyncio
from curl_cffi import AsyncSession

async with AsyncSession() as s:
    ws = await s.ws_connect("wss://echo.websocket.org")
    await asyncio.gather(*[ws.send_str("Hello, World!") for _ in range(10)])
    async for message in ws:
        print(message)

生态系统

4、pycurl

pycurl 文档:http://pycurl.io/docs/latest/
gevent 结合 pycurl:https://bitbucket.org/denis/gevent-curl/src/default/
不过这个源码在最新版本的gevent下无法运行,修复后的版本在这里:https://github.com/dytttf/gevent-pycurl

pycurl 简介

PycURL 是 libcurl(多协议文件传输库)的 Python 接口。与 urllib Python 模块类似,PycURL 可用于从 Python 程序中获取由 URL 标识的对象。然而,除了简单的获取之外,PycURL 还公开了 libcurl 的大部分功能,包括:

  • PycURL 是在 libcurl 之上的薄包装器,其实就是调用的C语言的 libcurl 库。速度非常快
  • 功能包括多种协议支持、SSL、身份验证和代理选项。PycURL 支持大多数 libcurl 的回调。
  • 用于网络操作的套接字,PycURL 可以集成到程序的 I/O 循环中(例如,使用 Tornado)。

Requests 与 PycURL 比较

  • PycURL 可以比 Requests 快几倍。当执行多个请求并重用连接时,性能差异会更大。
  • PycURL 可以通过 libcurl multi 接口利用 I/O 多路复用。
  • PycURL 支持许多协议,而不仅仅是 HTTP。
  • PycURL 通常提供更多功能,例如能够使用多个 TLS 后端、更多身份验证选项等。
  • Requests 是用纯 Python 编写的,不需要 C 扩展。因此,Requests 的安装非常简单,而 PycURL 的安装可能很复杂
  • Requests 的 API 通常比 PycURL 的 API 更易于学习和使用。

关于 libcurl

  • libcurl 是一个免费且易于使用的客户端 URL 传输库,支持 DICT、FILE、FTP、FTPS、Gopher、HTTP、HTTPS、IMAP、IMAPS、LDAP、LDAPS、POP3、POP3S、RTMP、RTSP、SCP、SFTP、SMTP、SMTPS、Telnet 和 TFTP。libcurl 支持 SSL 证书、HTTP POST、HTTP PUT、FTP 上传、基于 HTTP 表单的上传、代理、cookie、用户 + 密码认证(Basic、Digest、NTLM、Negotiate、Kerberos4)、文件传输恢复、http 代理隧道等!
  • libcurl 是免费的线程安全的IPv6 兼容的、功能丰富的支持的、快速的、有完整文档的,并且已经被许多知名的、大的和成功的公司和众多应用程序使用。

要求:libcurl 7.19.0 或更高版本。目前没有官方的二进制 Windows 包。可以从源代码构建 PycURL,也可以使用第三方二进制包。

linux 安装 curl: yum install curl
Python 安装模块:pip install pycurl

快速入门

快速入门:http://pycurl.io/docs/latest/quickstart.html

pycurl 的使用方法

c.setopt(pycurl.URL,myurl)            #(网址)
c.setopt(c.HTTPHEADER, http_header)   #网址头部
c.setopt(c.POST, 1)                   #1表示调用post方法而不是get
c.setopt(pycurl.POSTFIELDS,data)      #数据
c.setopt(pycurl.WRITEFUNCTION,my_func)#返回数据,进行回调
c.setopt(pycurl.CONNECTTIMEOUT,60)    #超时中断
c.setopt(pycurl.TIMEOUT,600)          #下载超时
c.perform()                           #提交

常用方法:

pycurl.Curl()                                               # 创建一个pycurl对象的方法
pycurl.Curl().setopt(pycurl.URL, http://www.pythontab.com)  # 设置要访问的URL
pycurl.Curl().setopt(pycurl.MAXREDIRS, 5)                   # 设置最大重定向次数
pycurl.Curl().setopt(pycurl.CONNECTTIMEOUT, 60)
pycurl.Curl().setopt(pycurl.TIMEOUT, 300)                   # 连接超时设置
 
# 模拟浏览器
pycurl.Curl().setopt(pycurl.USERAGENT, "Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)") 
 
pycurl.Curl().perform()                 # 服务器端返回的信息
pycurl.Curl().getinfo(pycurl.HTTP_CODE) # 查看HTTP的状态 类似urllib中status属性
pycurl.NAMELOOKUP_TIME                  # 域名解析时间
pycurl.CONNECT_TIME                     # 远程服务器连接时间
pycurl.PRETRANSFER_TIME                 # 连接上后到开始传输时的时间
pycurl.STARTTRANSFER_TIME               # 接收到第一个字节的时间
pycurl.TOTAL_TIME                       # 上一请求总的时间
pycurl.REDIRECT_TIME                    # 如果存在转向的话,花费的时间
pycurl.EFFECTIVE_URL
pycurl.HTTP_CODE HTTP                   # 响应代码
pycurl.REDIRECT_COUNT                   # 重定向的次数
pycurl.SIZE_UPLOAD                      # 上传的数据大小
pycurl.SIZE_DOWNLOAD                    # 下载的数据大小
pycurl.SPEED_UPLOAD                     # 上传速度
pycurl.HEADER_SIZE                      # 头部大小
pycurl.REQUEST_SIZE                     # 请求大小
pycurl.CONTENT_LENGTH_DOWNLOAD          # 下载内容长度
pycurl.CONTENT_LENGTH_UPLOAD            # 上传内容长度
pycurl.CONTENT_TYPE                     # 内容的类型
pycurl.RESPONSE_CODE                    # 响应代码
pycurl.SPEED_DOWNLOAD                   # 下载速度
pycurl.SSL_VERIFYRESULT
pycurl.INFO_FILETIME                    # 文件的时间信息
pycurl.HTTP_CONNECTCODE HTTP            # 连接代码
pycurl.HTTPAUTH_AVAIL
pycurl.PROXYAUTH_AVAIL
pycurl.OS_ERRNO
pycurl.NUM_CONNECTS
pycurl.SSL_ENGINES
pycurl.INFO_COOKIELIST
pycurl.LASTSOCKET
pycurl.FTP_ENTRY_PATH

示例 :

import StringIO
import pycurl
 
c = pycurl.Curl()
str = StringIO.StringIO()
c.setopt(pycurl.URL, "http://www.pythontab.com")
c.setopt(pycurl.WRITEFUNCTION, str.write)
c.setopt(pycurl.FOLLOWLOCATION, 1)  
c.perform()
print c.getinfo(pycurl.EFFECTIVE_URL)

5、下载 "图片、音乐、视频"

使用 requests 下载 图片

美女 图片

https://www.meitu131.com/shouji/meinv/
https://pixabay.com/zh/images/search/美女/
https://www.2meinv.cc/

# -*- coding: utf-8 -*-

import requests


def download_img():
    print("downloading with requests")
    
    # test_url = 'http://www.pythontab.com/test/demo.zip'
    # r = requests.get(test_url)
    # with open("./demo.zip", "wb") as ff:
    #     ff.write(r.content)

    img_url = 'https://img9.doubanio.com/view/celebrity/s_ratio_celebrity/public/p28424.webp'
    r = requests.get(img_url)
    with open("./img.jpg", "wb") as ff:
        ff.write(r.content)


if __name__ == '__main__':
    download_img()

爬取 校花网:http://www.xueshengmai.com/hua/ 大学校花 的图片
Python使用Scrapy爬虫框架全站爬取图片并保存本地(@妹子图@):https://www.cnblogs.com/william126/p/6923017.html

单线程版本

# -*- coding: utf-8 -*-

import os
import requests
# from PIL import Image
from lxml import etree


class Spider(object):
    """ crawl image """

    def __init__(self):
        self.index = 0
        self.url = "http://www.xueshengmai.com"
        # self.proxies = {
        #     "http": "http://172.17.18.80:8080",
        #     "https": "https://172.17.18.80:8080"
        # }
        pass

    def download_image(self, image_url):
        real_url = self.url + image_url
        print("downloading the {0} image".format(self.index))
        with open("./{0}.jpg".format(self.index), 'wb') as f:
            self.index += 1
            try:
                r = requests.get(
                    real_url,
                    # proxies=self.proxies
                )
                if 200 == r.status_code:
                    f.write(r.content)
            except BaseException as e:
                print(e)
        pass

    def add_url_prefix(self, image_url):
        return self.url + image_url

    def start_crawl(self):
        start_url = "http://www点xueshengmai点com/hua/"
        r = requests.get(
            start_url,
            # proxies=self.proxies
        )
        if 200 == r.status_code:
            temp = r.content.decode("gbk")
            html = etree.HTML(temp)
            links = html.xpath('//div[@class="item_t"]//img/@src')

            # url_list = list(map(lambda image_url=None: self.url + image_url, links))

            ###################################################################
            # python2
            # map(self.download_image, links)

            # python3 返回的是一个 map object ,所以需要 使用 list 包括下
            list(map(self.download_image, links))
            ###################################################################

            next_page_url = html.xpath(u'//div[@class="page_num"]//a[contains(text(),"下一页")]/@href')
            page_num = 2
            while next_page_url:
                print("download {0} page images".format(page_num))
                r_next = requests.get(
                    next_page_url[0],
                    # proxies=self.proxies
                )
                if r_next.status_code == 200:
                    html = etree.HTML(r_next.content.decode("gbk"))
                    links = html.xpath('//div[@class="item_t"]//img/@src')

                    # python3 返回的是一个 map object ,所以需要 使用 list 包括下
                    list(map(self.download_image, links))

                    try:
                        t_x_string = u'//div[@class="page_num"]//a[contains(text(),"下一页")]/@href'
                        next_page_url = html.xpath(t_x_string)
                    except BaseException as e:
                        next_page_url = None
                        # print e
                    page_num += 1
                    pass
                else:
                    print("response status code : {0}".format(r_next.status_code))
                pass
        else:
            print("response status code : {0}".format(r.status_code))
        pass


if __name__ == "__main__":
    t = Spider()
    t.start_crawl()
    pause = input("press any key to continue")
    pass

抓取 "妹子图"  代码:

# coding=utf-8

import requests
import os
from lxml import etree
import sys

'''
reload(sys)
sys.setdefaultencoding('utf-8')
'''


platform = 'Windows' if os.name == 'nt' else 'Linux'
print(f'当前系统是 【{platform}】 系统')

# http请求头
header = {
    # ':authority': 'www点mzitu点com',
    # ':method': 'GET',
    'accept': '*/*',
    'accept-encoding': 'gzip, deflate, br',
    'referer': 'https://www点mzitu点com',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                  'Chrome/75.0.3770.90 Safari/537.36'
}

site_url = 'http://www点mzitu点com'
url_prefix = 'http://www点mzitu点com/page/'
img_save_path = 'C:/mzitu/'


def get_page_max_num(page_html=None, flag=1):
    """

    :param page_html: 页面的 HTML 文本
    :param flag:  表示是 那个页面,1:所有妹子的列表页面。2:每个妹子单独的图片页面。
    :return:
    """
    # 找寻最大页数
    s_html = etree.HTML(page_html)

    xpath_string = '//div[@class="nav-links"]//a' if 1 == flag \
        else '//div[@class="pagenavi"]//a//span'

    display_page_link = s_html.xpath(xpath_string)
    # print(display_page_link[-1].text)
    max_num = display_page_link[-2].text if '下一页»' == display_page_link[-1].text \
        else display_page_link[-1].text
    return int(max_num)


def main():
    site_html = requests.get(site_url, headers=header).text
    page_max_num_1 = get_page_max_num(site_html)
    for page_num in range(1, page_max_num_1 + 1):
        page_url = f'{url_prefix}{page_num}'
        page_html = requests.get(page_url, headers=header).text
        s_page_html = etree.HTML(text=page_html)
        every_page_mm_url_list = s_page_html.xpath(
            '//ul[@id="pins"]//li[not(@class="box")]/span/a'
        )

        for tag_a in every_page_mm_url_list:
            mm_url = tag_a.get('href')
            title = tag_a.text.replace('\\', '').replace('/', '').replace(':', '')
            title = title.replace('*', '').replace('?', '').replace('"', '')
            title = title.replace('<', '').replace('>', '').replace('|', '')

            mm_dir = f'{img_save_path}{title}'
            if not os.path.exists(mm_dir):
                os.makedirs(mm_dir)

            print(f'【{title}】开始下载')
            mm_page_html = requests.get(mm_url, headers=header).text
            mm_page_max_num = get_page_max_num(mm_page_html, flag=2)
            for index in range(1, mm_page_max_num + 1):
                photo_url = f'{mm_url}/{index}'
                photo_html = requests.get(photo_url, headers=header).text
                s_photo_html = etree.HTML(text=photo_html)
                img_url = s_photo_html.xpath('//div[@class="main-image"]//img')[0].get('src')
                # print(img_url)
                r = requests.get(img_url, headers=header)
                if r.status_code == 200:
                    with open(f'{mm_dir}/{index}.jpg', 'wb') as f:
                        f.write(r.content)
                else:
                    print(f'status code : {r.status_code}')
            else:
                print(f'【{title}】下载完成')
        print(f'第【{page_num}】页完成')


if __name__ == '__main__':
    main()
    pass

运行成功后,会在脚本所在的目录 生成对应目录,每个目录里面都有对应的图片。。。。。

多线程版本。从 Python3.2开始,Python 标准库提供了 concurrent.futures 模块, concurrent.futures 模块可以利用 multiprocessing 实现真正的平行计算。python3 自带,python2 需要安装。

# coding=utf-8

import os
import sys
import requests
from lxml import etree
from concurrent import futures

'''
reload(sys)
sys.setdefaultencoding('utf-8')
'''

platform = 'Windows' if os.name == 'nt' else 'Linux'
print(f'当前系统是 【{platform}】 系统')


# http请求头
header = {
    # ':authority': 'www点mzitu点com',
    # ':method': 'GET',
    'accept': '*/*',
    'accept-encoding': 'gzip, deflate, br',
    'referer': 'https://www点mzitu点com',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
                  '(KHTML, like Gecko) Chrome/75.0.3770.90 Safari/537.36'                  
}


site_url = 'http://www点mzitu点com'
url_prefix = 'http://www点mzitu点com/page/'
img_save_path = 'C:/mzitu/'


def get_page_max_num(page_html=None, flag=1):
    """
    :param page_html: 页面的 HTML 文本
    :param flag:  表示是 那个页面,1:所有妹子的列表页面。2:每个妹子单独的图片页面。
    :return:
    """
    # 找寻最大页数
    s_html = etree.HTML(page_html)

    xpath_string = '//div[@class="nav-links"]//a' if 1 == flag \
        else '//div[@class="pagenavi"]//a//span'

    display_page_link = s_html.xpath(xpath_string)
    # print(display_page_link[-1].text)
    max_num = display_page_link[-2].text if '下一页»' == display_page_link[-1].text \
        else display_page_link[-1].text
    return int(max_num)


def download_img(args_info):
    img_url, mm_dir, index = args_info
    r = requests.get(img_url, headers=header)
    if r.status_code == 200:
        with open(f'{mm_dir}/{index}.jpg', 'wb') as f:
            f.write(r.content)
    else:
        print(f'status code : {r.status_code}')


def main():
    # 线程池中线程数
    with futures.ProcessPoolExecutor() as process_pool_executor:
        site_html = requests.get(site_url, headers=header).text
        page_max_num_1 = get_page_max_num(site_html)
        for page_num in range(1, page_max_num_1 + 1):
            page_url = f'{url_prefix}{page_num}'
            page_html = requests.get(page_url, headers=header).text
            s_page_html = etree.HTML(text=page_html)
            every_page_mm_url_list = s_page_html.xpath(
                '//ul[@id="pins"]//li[not(@class="box")]/span/a'
            )

            for tag_a in every_page_mm_url_list:
                mm_url = tag_a.get('href')
                title = tag_a.text.replace('\\', '').replace('/', '').replace(':', '')
                title = title.replace('*', '').replace('?', '').replace('"', '')
                title = title.replace('<', '').replace('>', '').replace('|', '')

                mm_dir = f'{img_save_path}{title}'
                if not os.path.exists(mm_dir):
                    os.makedirs(mm_dir)

                print(f'【{title}】开始下载')
                mm_page_html = requests.get(mm_url, headers=header).text
                mm_page_max_num = get_page_max_num(mm_page_html, flag=2)
                for index in range(1, mm_page_max_num + 1):
                    photo_url = f'{mm_url}/{index}'
                    photo_html = requests.get(photo_url, headers=header).text
                    s_photo_html = etree.HTML(text=photo_html)
                    img_url = s_photo_html.xpath('//div[@class="main-image"]//img')[0].get('src')

                    # 提交一个可执行的回调 task,它返回一个 Future 对象
                    process_pool_executor.submit(download_img, (img_url, mm_dir, index))
                else:
                    print(f'【{title}】下载完成')
            print(f'第【{page_num}】页完成')


if __name__ == '__main__':
    main()
    pass

显示 进度条

请求关键参数:stream=True。默认情况下,当你进行网络请求后,响应体会立即被下载。你可以通过 stream 参数覆盖这个行为,推迟下载响应体直到访问 Response.content 属性。

import json
import requests

tarball_url = 'https://github.com/kennethreitz/requests/tarball/master'

r = requests.get(tarball_url, stream=True)  # 此时仅有响应头被下载下来了,连接保持打开状态,响应体并没有下载。

print(json.dumps(dict(r.headers), ensure_ascii=False, indent=4))
# if int(r.headers['content-length']) < TOO_LONG:
#     content = r.content  # 只要访问 Response.content 属性,就开始下载响应体
#     # ...
#     pass

进一步使用 Response.iter_content 和 Response.iter_lines 方法来控制工作流,或者以 Response.raw 从底层 urllib3 的 urllib3.HTTPResponse

from contextlib import closing

with closing(requests.get('http://httpbin.org/get', stream=True)) as r:
    # Do things with the response here.
    pass

保持活动状态(持久连接) 。归功于 urllib3,同一会话内的持久连接是完全自动处理的,同一会话内发出的任何请求都会自动复用恰当的连接!注意:只有当响应体的所有数据被读取完毕时,连接才会被释放到连接池;所以确保将 stream 设置为 False 或读取 Response 对象的 content 属性。

在 Python3 中,print()方法的默认结束符(end=’\n’),当调用完之后,光标自动切换到下一行,此时就不能更新原有输出。将结束符改为 “\r” ,输出完成之后,光标会回到行首,并不换行。此时再次调用 print() 方法,就会更新这一行输出了。结束符也可以使用 “\d”,为退格符,光标回退一格,可以使用多个,按需求回退。在结束这一行输出时,将结束符改回 “\n” 或者不指定使用默认

下面是一个格式化的进度条显示模块。代码如下:

import requests
from contextlib import closing

"""
作者:微微寒
链接:https://www.zhihu.com/question/41132103/answer/93438156
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
"""


class ProgressBar(object):
    def __init__(
            self, title, count=0.0, run_status=None, fin_status=None,
            total=100.0, unit='', sep='/', chunk_size=1.0
    ):
        super(ProgressBar, self).__init__()
        self.info = "[%s] %s %.2f %s %s %.2f %s"
        self.title = title
        self.total = total
        self.count = count
        self.chunk_size = chunk_size
        self.status = run_status or ""
        self.fin_status = fin_status or " " * len(self.status)
        self.unit = unit
        self.seq = sep

    def __get_info(self):
        # 【名称】状态 进度 单位 分割线 总数 单位
        _info = self.info % (
            self.title, self.status, self.count/self.chunk_size, 
            self.unit, self.seq, self.total/self.chunk_size, self.unit
        )
        return _info

    def refresh(self, count=1, status=None):
        self.count += count
        # if status is not None:
        self.status = status or self.status
        end_str = "\r"
        if self.count >= self.total:
            end_str = '\n'
            self.status = status or self.fin_status
        print(self.__get_info(), end=end_str)


def main():
    with closing(requests.get("http://www.futurecrew.com/skaven/song_files/mp3/razorback.mp3", stream=True)) as response:
        chunk_size = 1024
        content_size = int(response.headers['content-length'])
        progress = ProgressBar(
            "razorback", total=content_size, unit="KB", 
            chunk_size=chunk_size, run_status="正在下载", fin_status="下载完成"
        )
        # chunk_size = chunk_size < content_size and chunk_size or content_size
        with open('./file.mp3', "wb") as file:
            for data in response.iter_content(chunk_size=chunk_size):
                file.write(data)
                progress.refresh(count=len(data))


if __name__ == '__main__':
    main()


Rich ( https://github.com/textualize/rich/blob/master/README.cn.md ) 是一个 Python 库,可以在终端中提供富文本和精美格式,还可以绘制漂亮的表格、进度条、markdown、语法高亮的源代码以及栈回溯信息(tracebacks)等。

断点 续传

断点续传:
https://www.leavesongs.com/PYTHON/resume-download-from-break-point-tool-by-python.html

Python实现下载界面(带进度条,断点续传,多线程多任务下载):https://blog.51cto.com/eddy72/2106091
视频下载以及断点续传( 使用 aiohttp 并发 ):https://www.cnblogs.com/baili-luoyun/p/10507608.html

另一种方法是调用 curl 之类支持断点续传的下载工具。

HTTP 断点续传原理

其实 HTTP 断点续传原理比较简单,在 HTTP 数据包中,可以增加 Range 头,这个头以字节为单位指定请求的范围,来下载范围内的字节流。如:

如上图勾下来的地方,我们发送数据包时选定请求的内容的范围,返回包即获得相应长度的内容。所以,我们在下载的时候,可以将目标文件分成很多“小块”,每次下载一小块(用Range标明小块的范围),直到把所有小块下载完。

当网络中断,或出错导致下载终止时,我们只需要记录下已经下载了哪些“小块”,还没有下载哪些。下次下载的时候在Range处填写未下载的小块的范围即可,这样就能构成一个断点续传。

其实像迅雷这种多线程下载器也是同样的原理。将目标文件分成一些小块,再分配给不同线程去下载,最后整合再检查完整性即可。

先看看这段文档:Advanced Usage — Requests 2.27.1 documentation,当请求时设置steam=True的时候就不会立即关闭连接,而我们以流的形式读取body,直到所有信息读取完全或者调用Response.close关闭连接。

所以,如果要下载大文件的话,就将 steam 设置为True,慢慢下载,而不是等整个文件下载完才返回。stackoverflow上有同学给出了一个简单的下载 demo:

#!/usr/bin/env python3

import requests


def download_file(url):
    local_filename = url.split('/')[-1]
    # NOTE the stream=True parameter
    r = requests.get(url, stream=True)
    with open(local_filename, 'wb') as f:
        for chunk in r.iter_content(chunk_size=1024): 
            if chunk:  # filter out keep-alive new chunks
                f.write(chunk)
                f.flush()
    return local_filename

这基本上就是我们核心的下载代码了。

  • 当使用 requests 的 get 下载大文件/数据时,建议使用使用 stream 模式。
  • 当把 get 函数的 stream 参数设置成 False 时,它会立即开始下载文件并放到内存中,如果文件过大,有可能导致内存不足。
  • 当把 get 函数的 stream 参数设置成 True 时,它不会立即开始下载,当你使用 iter_content 或 iter_lines 遍历内容或访问内容属性时才开始下载。需要注意一点:文件没有下载之前,它也需要保持连接。

iter_content:一块一块的遍历要下载的内容
iter_lines:一行一行的遍历要下载的内容

使用上面两个函数下载大文件可以防止占用过多的内存,因为每次只下载小部分数据。

示例代码:

r = requests.get(url_file, stream=True)
f = open("file_path", "wb")
for chunk in r.iter_content(chunk_size=512):
if chunk:
f.write(chunk)

断点续传结合大文件下载

先考虑一下需要注意的有哪些点,或者可以添加的功能有哪些:

  • 1. 用户自定义性:可以定义 cookie、referer、user-agent。如某些下载站检查用户登录才允许下载等情况。header中可能有filename,url中也有filename,用户还可以自己指定filename
  • 2. 很多服务端不支持断点续传,如何判断?
  • 3. 怎么去表达进度条?
  • 4. 如何得知文件的总大小?使用HEAD请求?那么服务器不支持HEAD请求怎么办?
  • 5. 下载后的文件名怎么处理?还要考虑windows不允许哪些字符做文件名。
  • 6. 如何去分块,是否加入多线程。
def download(self, url, filename, headers = {}):
    finished = False
    block = self.config['block']
    local_filename = self.remove_nonchars(filename)
    tmp_filename = local_filename + '.downtmp'
    if self.support_continue(url):  # 支持断点续传
        try:
            with open(tmp_filename, 'rb') as fin:
                self.size = int(fin.read()) + 1
        except:
            self.touch(tmp_filename)
        finally:
            headers['Range'] = "bytes=%d-" % (self.size, )
    else:
        self.touch(tmp_filename)
        self.touch(local_filename)

    size = self.size
    total = self.total
    r = requests.get(url, stream = True, verify = False, headers = headers)
    if total > 0:
        print "[+] Size: %dKB" % (total / 1024)
    else:
        print "[+] Size: None"
    start_t = time.time()
    with open(local_filename, 'ab') as f:
        try:
            for chunk in r.iter_content(chunk_size = block): 
                if chunk:
                    f.write(chunk)
                    size += len(chunk)
                    f.flush()
                sys.stdout.write('\b' * 64 + 'Now: %d, Total: %s' % (size, total))
                sys.stdout.flush()
            finished = True
            os.remove(tmp_filename)
            spend = int(time.time() - start_t)
            speed = int(size / 1024 / spend)
            sys.stdout.write('\nDownload Finished!\nTotal Time: %ss, Download Speed: %sk/s\n' % (spend, speed))
            sys.stdout.flush()

        except:
            import traceback
            print traceback.print_exc()
            print "\nDownload pause.\n"
        finally:
            if not finished:
                with open(tmp_filename, 'wb') as ftmp:
                    ftmp.write(str(size))

这是下载的方法。首先if语句调用 self.support_continue(url) 判断是否支持断点续传。如果支持则从一个临时文件中读取当前已经下载了多少字节,如果不存在这个文件则会抛出错误,那么size默认=0,说明一个字节都没有下载。

然后就请求url,获得下载连接,for循环下载。这个时候我们得抓住异常,一旦出现异常,不能让程序退出,而是正常将当前已下载字节size写入临时文件中。下次再次下载的时候读取这个文件,将Range设置成bytes=(size+1)-,也就是从当前字节的后一个字节开始到结束的范围。从这个范围开始下载,来实现一个断点续传。

判断是否支持断点续传的方法还兼顾了一个获得目标文件大小的功能:

def support_continue(self, url):
    headers = {
        'Range': 'bytes=0-4'
    }
    try:
        r = requests.head(url, headers = headers)
        crange = r.headers['content-range']
        self.total = int(re.match(ur'^bytes 0-4/(\d+)$', crange).group(1))
        return True
    except:
        pass
    try:
        self.total = int(r.headers['content-length'])
    except:
        self.total = 0
    return False

用正则匹配出大小,获得直接获取 headers['content-length'],获得将其设置为0.

核心代码:https://github.com/phith0n/py-wget/blob/master/py-wget.py

运行程序,获取 emlog 最新的安装包:

中间我按 Ctrl + C人工打断了下载进程,但之后还是继续下载,实现了“断点续传”。

但在我实际测试过程中,并不是那么多请求可以断点续传的,所以我对于不支持断点续传的文件这样处理:重新下载。

下载后的压缩包正常解压,也充分证明了下载的完整性:

动态图演示

github 地址:一个支持断点续传的小下载器:py-wget:GitHub - phith0n/py-wget: small wget by python

使用 you-get 下载 视频

python 示例代码( you-get 多线程 下载视频 ):


import os
import subprocess
from concurrent.futures import ThreadPoolExecutor, wait


def download(url):
    video_data_dir = './vide_data_dir'
    try:
        os.makedirs(video_data_dir)
    except BaseException as be:
        pass
    video_id = url.split('/')[-1]
    video_name = f'{video_data_dir}/{video_id}'
    command = f'you-get -o ./video_data -O {video_name} ' + url
    print(command)
    subprocess.call(command, shell=True)
    print(f"退出线程 ---> {url}")


def main():
    url_list = [
        'https://www.bilibili.com/video/BV1Xz4y127Yo',
        'https://www.bilibili.com/video/BV1yt4y1Q7SS',
        'https://www.bilibili.com/video/BV1bW411n7fY',
    ]
    with ThreadPoolExecutor(max_workers=3) as pool:
        thread_id_list = [pool.submit(download, url) for url in url_list]
        wait(thread_id_list)


if __name__ == '__main__':
    main()

you-get 帮助

D:\> you-get --help
说明:一个小型的下载程序,可以抓取web数据
用法: you-get [OPTION]... URL...

可选参数:
  -V, --version         版本
  -h, --help            帮助

抓取前可以设置的参数(不会实际去下载)
  -i, --info            打印提取的信息
  -u, --url             根据提供的url,打印提取的信息
  --json                以JSON格式打印提取的url

下载参数:
  -n, --no-merge        Do not merge video parts
  --no-caption          Do not download captions (subtitles, lyrics, danmaku, ...)
  -f, --force           强制重写存在的文件
  --skip-existing-file-size-check     跳过现有文件, 并且不会检查文件大小                        
  -F STREAM_ID, --format STREAM_ID    设置视频格式为STREAM_ID                       
  -O FILE, --output-filename FILE     设置输出的文件名                        
  -o DIR, --output-dir DIR            设置输出的目录                        
  -p PLAYER, --player PLAYER          提取流中的URL到一个播放器
                        
  -c COOKIES_FILE, --cookies COOKIES_FILE  载入cookies.txt 或者 cookies.sqlite
  -t SECONDS, --timeout SECONDS  设置 socket 超时时间
  -d, --debug                    显示回溯和其他调试信息
  -I FILE, --input-file FILE     从FILE中读取非播放列表url
                        
  -P PASSWORD, --password PASSWORD
                        视频访问密码设置为password
  -l, --playlist        下载播放列表
  -a, --auto-rename     自动重命名相同名称不同的文件
  -k, --insecure        忽略SSL错误

Playlist optional options:
  --first FIRST         the first number
  --last LAST           the last number
  --size PAGE_SIZE, --page-size PAGE_SIZE
                        the page size number

Proxy options:
  -x HOST:PORT, --http-proxy HOST:PORT
                        Use an HTTP proxy for downloading
  -y HOST:PORT, --extractor-proxy HOST:PORT
                        Use an HTTP proxy for extracting only
  --no-proxy            Never use a proxy
  -s HOST:PORT or USERNAME:PASSWORD@HOST:PORT, --socks-proxy HOST:PORT or USERNAME:PASSWORD@HOST:PORT
                        Use an SOCKS5 proxy for downloading
D:\>

探测视频真实的播放地址:

抓取前可以设置的参数(不会实际去下载)
  -i, --info            打印提取的信息
  -u, --url             根据提供的url,打印提取的信息
  --json                以JSON格式打印提取的url

示例:探测视频真实的播放地址

  • you-get -u https://www.bilibili.com/video/BV1Xz4y127Yo
  • you-get --json https://www.bilibili.com/video/BV1Xz4y127Yo

wget 库

相关库:https://pypi.org/search/?q=wget

使用 wget 命令:​wget http://www.robots.ox.ac.uk/~ankush/data.tar.gz

python 调用 wget 命令实现下载

使用 python 的 wget 模块:pip install wget

import wget
import tempfile

url = 'https://p0.ifengimg.com/2019_30/1106F5849B0A2A2A03AAD4B14374596C76B2BDAB_w1000_h626.jpg'

# 获取文件名
file_name = wget.filename_from_url(url)
print(file_name)  #1106F5849B0A2A2A03AAD4B14374596C76B2BDAB_w1000_h626.jpg

# 下载文件,使用默认文件名,结果返回文件名
file_name = wget.download(url)
print(file_name) #1106F5849B0A2A2A03AAD4B14374596C76B2BDAB_w1000_h626.jpg

# 下载文件,重新命名输出文件名
target_name = 't1.jpg'
file_name = wget.download(url, out=target_name)
print(file_name) #t1.jpg

# 创建临时文件夹,下载到临时文件夹里
tmpdir = tempfile.gettempdir()
target_name = 't2.jpg'
file_name = wget.download(url, out=os.path.join(tmpdir, target_name))
print(file_name)  #/tmp/t2.jpg

ffmpeg

ffmpeg -ss 00:00:00 -i "https://vd4.bdstatic.com/mda-na67uu3bf6v85cnm/sc/cae_h264/1641533845968105062/mda-na67uu3bf6v85cnm.mp4?v_from_s=hkapp-haokan-hbe&auth_key=1641555906-0-0-642c8f9b47d4c37cc64d307be88df29d&bcevod_channel=searchbox_feed&pd=1&pt=3&logid=0906397151&vid=8050108300345362998&abtest=17376_2&klogid=0906397151" -t 00:05:00 -c copy "test.mp4"

ffmpeg 如何设置 header 信息

ffmpeg -user_agent "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36" -headers "sec-ch-ua: 'Chromium';v='88', 'Google Chrome';v='88', ';Not A Brand';v='99'"$'\r\n'"sec-ch-ua-mobile: ?0"$"Upgrade-Insecure-Requests: 1"  -i http://127.0.0.1:3000

如果只需要 ua 只加上 -user_agent 就可以。如果需要设置 -headers 其他选项时,多个选项用 $'\r\n' 链接起来。服务端接收数据格式正常,如图

ffmpeg 设置 header 请求头 UA 文件最大大小

ffmpeg -headers $'Origin: https://xxx.com\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36\r\nReferer: https://xxx.com' -threads 0 -i '地址' -c copy -y -f mpegts '文件名.ts' -v trace

使用-headers $’头一\r\n头二’添加header
注意顺序 ,放在命令行最后面无法生效!!!!!
后来输出了一下信息才发现问题
-v trace 用于输出当前的header信息方便调试
设置 UA 可以使用单独的 -user-agent 指令
在输出文件名前使用 -fs 1024K 限制为 1024K

ffmpeg 帮助:ffmpeg --help

Getting help:
    -h      -- print basic options
    -h long -- print more options
    -h full -- print all options (including all format and codec specific options, very long)
    -h type=name -- print all options for the named decoder/encoder/demuxer/muxer/filter/bsf/protocol
    See man ffmpeg for detailed description of the options.

Print help / information / capabilities:
-L                  show license
-h topic            show help
-? topic            show help
-help topic         show help
--help topic        show help
-version            show version
-buildconf          show build configuration
-formats            show available formats
-muxers             show available muxers
-demuxers           show available demuxers
-devices            show available devices
-codecs             show available codecs
-decoders           show available decoders
-encoders           show available encoders
-bsfs               show available bit stream filters
-protocols          show available protocols
-filters            show available filters
-pix_fmts           show available pixel formats
-layouts            show standard channel layouts
-sample_fmts        show available audio sample formats
-dispositions       show available stream dispositions
-colors             show available color names
-sources device     list sources of the input device
-sinks device       list sinks of the output device
-hwaccels           show available HW acceleration methods

Global options (affect whole program instead of just one file):
-loglevel loglevel  set logging level
-v loglevel         set logging level
-report             generate a report
-max_alloc bytes    set maximum size of a single allocated block
-y                  overwrite output files
-n                  never overwrite output files
-ignore_unknown     Ignore unknown stream types
-filter_threads     number of non-complex filter threads
-filter_complex_threads  number of threads for -filter_complex
-stats              print progress report during encoding
-max_error_rate maximum error rate  ratio of decoding errors (0.0: no errors, 1.0: 100% errors) above which ffmpeg returns an error instead of success.
-vol volume         change audio volume (256=normal)

Per-file main options:
-f fmt              force format
-c codec            codec name
-codec codec        codec name
-pre preset         preset name
-map_metadata outfile[,metadata]:infile[,metadata]  set metadata information of outfile from infile
-t duration         record or transcode "duration" seconds of audio/video
-to time_stop       record or transcode stop time
-fs limit_size      set the limit file size in bytes
-ss time_off        set the start time offset
-sseof time_off     set the start time offset relative to EOF
-seek_timestamp     enable/disable seeking by timestamp with -ss
-timestamp time     set the recording timestamp ('now' to set the current time)
-metadata string=string  add metadata
-program title=string:st=number...  add program with specified streams
-target type        specify target file type ("vcd", "svcd", "dvd", "dv" or "dv50" with optional prefixes "pal-", "ntsc-" or "film-")
-apad               audio pad
-frames number      set the number of frames to output
-filter filter_graph  set stream filtergraph
-filter_script filename  read stream filtergraph description from a file
-reinit_filter      reinit filtergraph on input parameter changes
-discard            discard
-disposition        disposition

Video options:
-vframes number     set the number of video frames to output
-r rate             set frame rate (Hz value, fraction or abbreviation)
-fpsmax rate        set max frame rate (Hz value, fraction or abbreviation)
-s size             set frame size (WxH or abbreviation)
-aspect aspect      set aspect ratio (4:3, 16:9 or 1.3333, 1.7777)
-vn                 disable video
-vcodec codec       force video codec ('copy' to copy stream)
-timecode hh:mm:ss[:;.]ff  set initial TimeCode value.
-pass n             select the pass number (1 to 3)
-vf filter_graph    set video filters
-ab bitrate         audio bitrate (please use -b:a)
-b bitrate          video bitrate (please use -b:v)
-dn                 disable data

Audio options:
-aframes number     set the number of audio frames to output
-aq quality         set audio quality (codec-specific)
-ar rate            set audio sampling rate (in Hz)
-ac channels        set number of audio channels
-an                 disable audio
-acodec codec       force audio codec ('copy' to copy stream)
-vol volume         change audio volume (256=normal)
-af filter_graph    set audio filters

Subtitle options:
-s size             set frame size (WxH or abbreviation)
-sn                 disable subtitle
-scodec codec       force subtitle codec ('copy' to copy stream)
-stag fourcc/tag    force subtitle tag/fourcc
-fix_sub_duration   fix subtitles duration
-canvas_size size   set canvas size (WxH or abbreviation)
-spre preset        set the subtitle options to the indicated preset

多线程 下载 文件

示例代码:

# 在python3下测试
 
import sys
import requests
import threading
import datetime
 
# 传入的命令行参数,要下载文件的url
url = sys.argv[1]
 
 
def Handler(start, end, url, filename):
    
    headers = {'Range': 'bytes=%d-%d' % (start, end)}
    r = requests.get(url, headers=headers, stream=True)
    
    # 写入文件对应位置
    with open(filename, "r+b") as fp:
        fp.seek(start)
        var = fp.tell()
        fp.write(r.content)
 
 
def download_file(url, num_thread = 5):
    
    r = requests.head(url)
    try:
        file_name = url.split('/')[-1]
        file_size = int(r.headers['content-length'])   # Content-Length获得文件主体的大小,当http服务器使用Connection:keep-alive时,不支持Content-Length
    except:
        print("检查URL,或不支持对线程下载")
        return
 
    #  创建一个和要下载文件一样大小的文件
    fp = open(file_name, "wb")
    fp.truncate(file_size)
    fp.close()
 
    # 启动多线程写文件
    part = file_size // num_thread  # 如果不能整除,最后一块应该多几个字节
    for i in range(num_thread):
        start = part * i
        if i == num_thread - 1:   # 最后一块
            end = file_size
        else:
            end = start + part
 
        t = threading.Thread(target=Handler, kwargs={'start': start, 'end': end, 'url': url, 'filename': file_name})
        t.setDaemon(True)
        t.start()
 
    # 等待所有线程下载完成
    main_thread = threading.current_thread()
    for t in threading.enumerate():
        if t is main_thread:
            continue
        t.join()
    print('%s 下载完成' % file_name)
 
if __name__ == '__main__':
    start = datetime.datetime.now().replace(microsecond=0)  
    download_file(url)
    end = datetime.datetime.now().replace(microsecond=0)
    print("用时: ", end='')
    print(end-start)

下载 "图片、音乐、视频"

# -*- coding:utf-8 -*-

import re
import requests
from contextlib import closing
from lxml import etree


class Spider(object):
    """ crawl image """
    def __init__(self):
        self.index = 0
        self.url = "http://www.xiaohuar.com"
        self.proxies = {"http": "http://172.17.18.80:8080", "https": "https://172.17.18.80:8080"}
        pass

    def download_image(self, image_url):
        real_url = self.url + image_url
        print "downloading the {0} image".format(self.index)
        with open("{0}.jpg".format(self.index), 'wb') as f:
            self.index += 1
            f.write(requests.get(real_url, proxies=self.proxies).content)
            pass
        pass

    def start_crawl(self):
        start_url = "http://www.xiaohuar.com/hua/"
        r = requests.get(start_url, proxies=self.proxies)
        if r.status_code == 200:
            temp = r.content.decode("gbk")
            html = etree.HTML(temp)
            links = html.xpath('//div[@class="item_t"]//img/@src')
            map(self.download_image, links)
            # next_page_url = html.xpath('//div[@class="page_num"]//a/text()')
            # print next_page_url[-1]
            # print next_page_url[-2]
            # print next_page_url[-3]
            next_page_url = html.xpath(u'//div[@class="page_num"]//a[contains(text(),"下一页")]/@href')
            page_num = 2
            while next_page_url:
                print "download {0} page images".format(page_num)
                r_next = requests.get(next_page_url[0], proxies=self.proxies)
                if r_next.status_code == 200:
                    html = etree.HTML(r_next.content.decode("gbk"))
                    links = html.xpath('//div[@class="item_t"]//img/@src')
                    map(self.download_image, links)
                    try:
                        next_page_url = html.xpath(u'//div[@class="page_num"]//a[contains(text(),"下一页")]/@href')
                    except BaseException as e:
                        next_page_url = None
                        print e
                    page_num += 1
                    pass
                else:
                    print "response status code : {0}".format(r_next.status_code)
                pass
        else:
            print "response status code : {0}".format(r.status_code)
        pass


class ProgressBar(object):
    def __init__(self, title, count=0.0, run_status=None, fin_status=None, total=100.0, unit='', sep='/', chunk_size=1.0):
        super(ProgressBar, self).__init__()
        self.info = "[%s] %s %.2f %s %s %.2f %s"
        self.title = title
        self.total = total
        self.count = count
        self.chunk_size = chunk_size
        self.status = run_status or ""
        self.fin_status = fin_status or " " * len(self.status)
        self.unit = unit
        self.seq = sep

    def __get_info(self):
        # 【名称】状态 进度 单位 分割线 总数 单位
        _info = self.info % (self.title, self.status,
                             self.count / self.chunk_size, self.unit, self.seq, self.total / self.chunk_size, self.unit)
        return _info

    def refresh(self, count=1, status=None):
        self.count += count
        # if status is not None:
        self.status = status or self.status
        end_str = "\r"
        if self.count >= self.total:
            end_str = '\n'
            self.status = status or self.fin_status
        print self.__get_info(), end_str


def download_mp4(video_url):
    print video_url
    try:
        with closing(requests.get(video_url.strip().decode(), stream=True)) as response:
            chunk_size = 1024
            with open('./{0}'.format(video_url.split('/')[-1]), "wb") as f:
                for data in response.iter_content(chunk_size=chunk_size):
                    f.write(data)
                    f.flush()

    except BaseException as e:
        print e
        return


def mp4():
    proxies = {"http": "http://172.17.18.80:8080", "https": "https://172.17.18.80:8080"}
    url = "http://www.budejie.com/video/"
    r = requests.get(url)
    print r.url
    if r.status_code == 200:
        print "status_code:{0}".format(r.status_code)
        content = r.content
        video_urls_compile = re.compile("http://.*?\.mp4")
        video_urls = re.findall(video_urls_compile, content)
        print len(video_urls)
        # print video_urls
        map(download_mp4, video_urls)
    else:
        print "status_code:{0}".format(r.status_code)


def mp3():
    proxies = {"http": "http://172.17.18.80:8080", "https": "https://172.17.18.80:8080"}
    with closing(requests.get("http://www.futurecrew.com/skaven/song_files/mp3/razorback.mp3", proxies=proxies, stream=True)) as response:
        chunk_size = 1024
        content_size = int(response.headers['content-length'])
        progress = ProgressBar("razorback", total=content_size, unit="KB", chunk_size=chunk_size, run_status="正在下载",
                               fin_status="下载完成")
        # chunk_size = chunk_size < content_size and chunk_size or content_size
        with open('./file.mp3', "wb") as f:
            for data in response.iter_content(chunk_size=chunk_size):
                f.write(data)
                progress.refresh(count=len(data))


if __name__ == "__main__":
    t = Spider()
    t.start_crawl()   
    mp3()
    mp4()   
    pass

下载视频的效果

另一个下载图片示例代码:

( github 地址:https://github.com/injetlee/Python/blob/master/爬虫集合/meizitu.py )

包括了创建文件夹,利用多线程爬取,设置的是5个线程,可以根据自己机器自己来设置一下。

import requests
import os
import time
import threading
from bs4 import BeautifulSoup


def download_page(url):
   '''
   用于下载页面
   '''
   headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0"}
   r = requests.get(url, headers=headers)
   r.encoding = 'gb2312'
   return r.text


def get_pic_list(html):
   '''
   获取每个页面的套图列表,之后循环调用get_pic函数获取图片
   '''
   soup = BeautifulSoup(html, 'html.parser')
   pic_list = soup.find_all('li', class_='wp-item')
   for i in pic_list:
       a_tag = i.find('h3', class_='tit').find('a')
       link = a_tag.get('href')
       text = a_tag.get_text()
       get_pic(link, text)


def get_pic(link, text):
   '''
   获取当前页面的图片,并保存
   '''
   html = download_page(link)  # 下载界面
   soup = BeautifulSoup(html, 'html.parser')
   pic_list = soup.find('div', id="picture").find_all('img')  # 找到界面所有图片
   headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0"}
   create_dir('pic/{}'.format(text))
   for i in pic_list:
       pic_link = i.get('src')  # 拿到图片的具体 url
       r = requests.get(pic_link, headers=headers)  # 下载图片,之后保存到文件
       with open('pic/{}/{}'.format(text, link.split('/')[-1]), 'wb') as f:
           f.write(r.content)
           time.sleep(1)   # 休息一下,不要给网站太大压力,避免被封


def create_dir(name):
   if not os.path.exists(name):
       os.makedirs(name)


def execute(url):
   page_html = download_page(url)
   get_pic_list(page_html)


def main():
   create_dir('pic')
   queue = [i for i in range(1, 72)]   # 构造 url 链接 页码。
   threads = []
   while len(queue) > 0:
       for thread in threads:
           if not thread.is_alive():
               threads.remove(thread)
       while len(threads) < 5 and len(queue) > 0:   # 最大线程数设置为 5
           cur_page = queue.pop(0)
           url = 'http://meizitu.com/a/more_{}.html'.format(cur_page)
           thread = threading.Thread(target=execute, args=(url,))
           thread.setDaemon(True)
           thread.start()
           print('{}正在下载{}页'.format(threading.current_thread().name, cur_page))
           threads.append(thread)


if __name__ == '__main__':
   main()

6、websockets

pypi:https://pypi.org/search/?q=websockets

安装:pip install websockets

websockets 是一个用于构建 WebSocket 服务器和客户端的 Python 库,它实现了 RFC 6455 标准,提供了异步、高性能的 WebSocket 通信功能。

websockets 简介

WebSocket 是 HTML5 中定义的协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯,WebSocket 是一种在单个TCP连接上进行全双工通信的协议。它使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

WebSocket 握手协议:WebSocket 与 HTTP 不同,WebSocket协议只需要发送一次连接请求,请求的完整过程被称为握手,即客户端为了创建WebSocket连接而向服务器发送特定的 http 请求并声明升级协议。WebSocket 是独立的、创建在 TCP 上的协议,通过HTTP/1.1 协议的101状态码进行握手,握手成功后才会转为WebSocket协议。

WebSocket 通信过程:

  • 1. 客户端发起握手请求
  • 2. 服务器端收到请求后验证并返回握手结果
  • 3. 连接建立成功,服务器端开始推送消息

Web 领域中,用于实现数据'实时'更新的手段有轮询和 WebSocket 这两种。轮询指的是客户端按照一定时间间隔(如 1 秒)访问服务端接口,从而达到 '实时' 的效果,虽然看起来数据像是实时更新的,但实际上它有一定的时间间隔,并不是真正的实时更新。轮询通常采用 拉 模式,由客户端主动从服务端拉取数据。

WebSocket 采用的是 推 模式,由服务端主动将数据推送给客户端,这种方式是真正的实时更新。使用场景:比如体育赛事实时数据、股市实时数据或币圈实时变化的数据。

使用 websockets 的网页特点

  • 1. 打开开发者模式,检查网页源代码会发现,源代码中并没有想要的数据。
  • 2. 观察是否是使用AJAX请求来更新数据,如果没有新的AJAX请求发出,但是数据是实时更新的,说明该网站并不是使用轮询的方式更新数据。
  • 3. 如果没有使用 AJAX,可以点开开发这工具上的 socket(套接字) 选项卡,就可以看到数据。

示例:脚本之家 - 区块链

URL 地址:https://www.jb51.net/coin/

在开发者工具中提供了筛选功能,其中 WS(套接字) 代表只显示 WebSocket 连接的网络请求。

WS(套接字) 与 HTTP 请求 url 不同,WebSocket 连接地址以 ws 或 wss 开头。连接成功的状态码不是 200 而是 101

message 标签页是双方传递的数据

  • 绿色 ⬆ :是客户端发送给服务端的数据
  • 红色 ⬇ :是服务端发送给客户端的数据

从 数据顺序 可知:客户端先发送数据,然后服务器端回应 ok,然后服务器端一直推送数据。

所以,从发起握手到获得数据的整个流程为:

那么,现在问题来了:

  • 握手怎么弄?
  • 连接保持怎么弄?
  • 消息发送和接收怎么弄?
  • 有什么库可以轻松实现吗?

分析解密数据

第一个 wss://wss.jbzj.com/currency,

点击进入,并打上断点

进入 t._addListeners() 函数

可以看到添加了几个事件处理函数,this._ws.addEventListener("message", this._handleMessage), 就是处理 message 的,进入 this._handleMessage 函数查看

onmessage 就是处理消息的,进入 onmessage

可以看到 onmessage 是在 ontimedata 这个函数中定义的。

  • 通过打上断点,可以验证
  • 也可以通过添加监控变量来验证

第2个 wss://wss.jbzj.com/currency

可以看到 ontimedata 从字面意思可以知道是 "实时数据",进入 ontimedata 函数

通过查看代码,可以找到 onmessage 函数就是处理接收的数据。

直接 定位 WebSocket 

有的 WebSocket 客户端请求发送的数据非常简单,有的WebSocket 客户端发送的数据是 Binary Message(二进制数据)、或者更复杂的加密参数,直接搜索无法获取,针对这种情况,也有解决方法:

  1. 已知创建 WebSocket 对象的语句为:var Socket = new WebSocket(url, [protocol] );,所以可以搜索 new WebSocket 定位到建立请求的位置。
  2. 已知一个 WebSocket 对象有以下相关事件,所以可以搜索对应事件处理程序代码来定位:
事件事件处理程序描述
openSocket.onopen连接建立时触发
messageSocket.onmessage客户端接收服务端数据时触发
errorSocket.onerror通信发生错误时触发
closeSocket.onclose连接关闭时触发

已知一个 WebSocket 对象有以下相关方法,所以可以搜索对应方法来定位:

方法描述
Socket.send()使用连接发送数据
Socket.close()关闭连接

websocker 测试工具

  • 浏览器插件:WebSocket DevTools
  • api 测试工具:Postman、Apifox、Apipost

代码 实现

先打个桩,有时间搞。。。

7、Python 实现 WebSocket 请求

pip install websocket

Python 中最常用的网络请求库是 Requests,但是它是基于 HTTP 协议的,在面对 WebSocket 协议时就无法使用,Python 库中用于连接 WebSocket 的有很多,但是易用、稳定的有

注意:

  • 使用 websockets 时,python 导包用的 import websockets
  • 使用 websocket-client 时,python 导包用的 import websocket

客户端 使用详解

1. 基本连接和通信

import asyncio
import websockets

async def client_example():
    uri = "ws://localhost:8765"
    async with websockets.connect(uri) as websocket:
        # 发送消息
        await websocket.send("Hello Server!")
        
        # 接收消息
        response = await websocket.recv()
        print(f"Received: {response}")

# 运行客户端
asyncio.run(client_example())

2. 客户端配置选项

async def advanced_client():
    uri = "wss://example.com/websocket"  # 使用 SSL/TLS
    
    async with websockets.connect(
        uri,
        extra_headers={"Authorization": "Bearer token"},  # 自定义头部
        ping_interval=20,      # ping 间隔(秒)
        ping_timeout=10,       # ping 超时时间
        close_timeout=5,       # 关闭超时时间
        max_size=2**20,        # 最大消息大小(1MB)
        max_queue=2**5,        # 最大消息队列长度
    ) as websocket:
        await websocket.send("Hello")
        response = await websocket.recv()
        print(response)

3. 处理连接异常

import websockets
import asyncio

async def robust_client():
    try:
        async with websockets.connect("ws://localhost:8765") as websocket:
            await websocket.send("Hello")
            response = await websocket.recv()
            print(response)
    except websockets.exceptions.ConnectionClosed:
        print("连接已关闭")
    except websockets.exceptions.InvalidURI:
        print("无效的 URI")
    except Exception as e:
        print(f"发生错误: {e}")

服务器端 使用详解

1. 基本服务器实现

import asyncio
import websockets

async def echo(websocket, path):
    async for message in websocket:
        await websocket.send(f"Echo: {message}")

# 启动服务器
start_server = websockets.serve(echo, "localhost", 8765)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()

2. 更复杂的服务器示例

import asyncio
import websockets
import json

# 存储连接的客户端
clients = set()

async def register(websocket):
    clients.add(websocket)
    print(f"客户端 {websocket.remote_address} 已连接")

async def unregister(websocket):
    clients.remove(websocket)
    print(f"客户端 {websocket.remote_address} 已断开")

async def broadcast(message):
    """向所有连接的客户端广播消息"""
    if clients:
        await asyncio.wait([client.send(message) for client in clients])

async def handler(websocket, path):
    await register(websocket)
    try:
        async for message in websocket:
            data = json.loads(message)
            if data["type"] == "chat":
                # 广播聊天消息
                await broadcast(json.dumps({
                    "type": "chat",
                    "username": data["username"],
                    "message": data["message"]
                }))
    except websockets.exceptions.ConnectionClosed:
        pass
    finally:
        await unregister(websocket)

# 启动服务器
start_server = websockets.serve(handler, "localhost", 8765)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()

3. 服务器配置选项

async def advanced_server():
    async def handler(websocket, path):
        await websocket.send("Hello!")

    # 服务器配置
    server = await websockets.serve(
        handler,
        "localhost",
        8765,
        ping_interval=20,      # ping 间隔
        ping_timeout=10,       # ping 超时
        close_timeout=5,       # 关闭超时
        max_size=2**20,        # 最大消息大小
        max_queue=2**5,        # 最大队列长度
        read_limit=2**16,      # 读取缓冲区大小
        write_limit=2**16,     # 写入缓冲区大小
    )
    
    await server.wait_closed()

消息处理 详解

1. 发送不同类型的消息

async def message_types(websocket):
    # 发送文本消息
    await websocket.send("这是一个文本消息")
    
    # 发送二进制消息
    binary_data = b"\x00\x01\x02\x03"
    await websocket.send(binary_data)
    
    # 发送 JSON 数据
    import json
    data = {"type": "update", "value": 42}
    await websocket.send(json.dumps(data))

2. 接收和处理不同类型的消息

async def receive_messages(websocket):
    async for message in websocket:
        # 检查消息类型
        if isinstance(message, str):
            print(f"收到文本消息: {message}")
        elif isinstance(message, bytes):
            print(f"收到二进制消息: {message}")

异常 处理

常见异常类型

import websockets.exceptions

async def handle_exceptions(websocket):
    try:
        await websocket.send("Hello")
        response = await websocket.recv()
    except websockets.exceptions.ConnectionClosed:
        print("连接已关闭")
    except websockets.exceptions.ConnectionClosedError:
        print("连接异常关闭")
    except websockets.exceptions.ConnectionClosedOK:
        print("连接正常关闭")
    except websockets.exceptions.InvalidHandshake:
        print("握手失败")
    except websockets.exceptions.InvalidMessage:
        print("无效消息")
    except websockets.exceptions.InvalidStatusCode:
        print("无效状态码")
    except websockets.exceptions.NegotiationError:
        print("协商错误")
    except websockets.exceptions.RedirectHandshake:
        print("重定向握手")
    except websockets.exceptions.SecurityError:
        print("安全错误")

实际应用示例:简单聊天应用

# 客户端示例
import asyncio
import websockets
import json

async def chat_client():
    uri = "ws://localhost:8765"
    async with websockets.connect(uri) as websocket:
        # 发送用户名
        username = input("请输入用户名: ")
        
        # 发送消息循环
        while True:
            message = input("输入消息 (输入 'quit' 退出): ")
            if message == "quit":
                break
                
            await websocket.send(json.dumps({
                "type": "chat",
                "username": username,
                "message": message
            }))
            
            # 接收响应
            response = await websocket.recv()
            print(f"服务器响应: {response}")

# 运行客户端
# asyncio.run(chat_client())

最佳实践

  • 使用异步上下文管理器:
       async with websockets.connect(uri) as websocket:
           # 自动处理连接和关闭
  • 设置合适的超时时间:
       # 避免无限等待
       websocket.recv(timeout=10)
  • 处理连接异常:
       try:
           async with websockets.connect(uri) as ws:
               # 通信逻辑
       except websockets.exceptions.ConnectionClosed:
           # 重新连接逻辑

示例:上证综合指数

上证综合指数:https://finance.sina.com.cn/realstock/company/sh000001/nc.shtml

通过 apipost 模拟发送数据

请求头中有几个参数,websockets 会自动生成,python 编程时候不需要传递

python 代码实现

import asyncio
import websockets


async def connect_to_websocket():
    ws_url = 'wss://hq.sinajs.cn/wskt?list=sh000001'
    # 设置请求头
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
                      '(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0',    
        'origin': 'https://finance.sina.com.cn',
    }

    async with websockets.connect(ws_url, additional_headers=headers) as websocket:
        # 不需要发送数据,所以这行注释掉
        # await websocket.send('{"action": "subscribe", "topic": "data"}')
        while True:
            resp_data = await websocket.recv()  # 接收消息
            print(resp_data)  # 打印解析后的数据


# 启动事件循环
asyncio.run(connect_to_websocket())

运行结果:

并行执行

import asyncio
import websockets


async def send_msg(ws, msg):
    await ws.send(msg)

async def receive_msg(ws):
    while True:
        msg = await ws.recv()
        print(msg)


async def connect_to_websocket():
    ws_url = 'wss://hq.sinajs.cn/wskt?list=sh000001'
    # 设置请求头
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
                      '(KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0',
        'origin': 'https://finance.sina.com.cn',
    }

    async with websockets.connect(ws_url, additional_headers=headers) as ws:
        t1 = asyncio.create_task(send_msg(ws, 'hello'))
        t2 = asyncio.create_task(receive_msg(ws))
        # 只有放到task中,然后使用wait才会并行执行
        await asyncio.wait([t1, t2])

# 启动事件循环
asyncio.run(connect_to_websocket())

示例:格力电器

格力电器:https://finance.sina.com.cn/realstock/company/sz000651/nc.shtml

pip install websocket-client

websocket-client 文档:https://websocket-client.readthedocs.io/en/latest/

通过文档可以发现,如果想在不编写任何代码的情况下连接到 websocket, 可以试试 入门wsdump.py 脚本和 examples/ 目录文件。

注意:

  • 使用 websockets 时,python 导包用的 import websockets
  • 使用 websocket-client 时,python 导包用的 import websocket

短链接 (WebSocket )

WebSocket 最适合 短期连接

长连接 (WebSocketApp )

WebSocketApp 最适合长期连接 连接。

示例:爬虫 代码 格式

import requests
import websocket
import json
import time
import math


def getToken():
    """
    获取加密字符串,将其拼接到websocket协议的url上
    :return: token
    """
    url = "https://live.611.com/Live/GetToken"
    response = requests.get(url)
    if response.status_code == 200:
        data = json.loads(response.text)
        token = data["Data"]
        return token
    else:
        print("请求错误")


def get_message():
    """
    构造websocket的验证信息
    :return: message1,message2
    """
    _time = math.floor(time.time()) * 1000
    info = {'chrome': 'true', 'version': '80.0.3987.122', 'webkit': 'true'}
    message1 = {
        "command": "RegisterInfo",
        "action": "Web",
        "ids": [],
        "UserInfo": {
            "Version": str([_time]) + json.dumps(info),
            "Url": "https://live.611.com/zq"
        }
    }
    message2 = {
        "command": "JoinGroup",
        "action": "SoccerLiveOdd",
        "ids": []
    }
    return json.dumps(message1), json.dumps(message2)


def Download(token,message1,message2):
    """
    抓取数据
    :param token: token
    :param message1: message1
    :param message2: message2
    :return: 
    """
    uri = "wss://push.611.com:6119/{}".format(token)
    ws = websocket.create_connection(uri, timeout=10)
    ws.send(message1)
    ws.send(message2)
    while True:
        result = ws.recv()
        print(result)

if __name__ == '__main__':
    token = getToken() # 获取token字符串
    message1, message2 = get_message() # 构造请求信息
    Download(token,message1, message2) # 抓取数据

示例 2:https://segmentfault.com/a/1190000041079154

在 websocket-client 官方文档中给我们提供了一个长连接的 demo,它实现了连续发送三次数据,并实时监听服务端返回的数据,其中的 websocket.enableTrace(True) 表示是否显示连接详细信息:

import websocket
import _thread
import time


def on_message(ws, message):
    print(message)


def on_error(ws, error):
    print(error)


def on_close(ws, close_status_code, close_msg):
    print("### closed ###")


def on_open(ws):
    def run(*args):
        for i in range(3):
            time.sleep(1)
            ws.send("Hello %d" % i)
        time.sleep(1)
        ws.close()
        print("thread terminating...")
    _thread.start_new_thread(run, ())


if __name__ == "__main__":
    websocket.enableTrace(True)
    ws = websocket.WebSocketApp(
        "ws://echo.websocket.org/", on_open=on_open,
        on_message=on_message, on_error=on_error, on_close=on_close
    )

    ws.run_forever()

改造后代码:以下只演示部分关键代码,不能直接运行! 完整代码仓库地址:https://github.com/kgepachong/crawler/

import time
import json
import base64
import _thread
import requests
import websocket
from PIL import Image


web_socket_url = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
get_login_qr_img_url = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
login_url = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
user_info_url = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"

headers = {
    "Host": "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler",
    "Pragma": "no-cache",
    "Referer": "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36"
}

qr_token = ""
once_password = ""
uuid = ""
cookie = {}


def get_cookies_first():
    response = requests.get(url=login_url, headers=headers)
    global cookie
    cookie = response.cookies.get_dict()


def get_login_qr_img():
    response = requests.get(url=get_login_qr_img_url, headers=headers, cookies=cookie).json()
    qr_img = response["img"]
    global qr_token
    qr_token = response["qrToken"]
    with open('code.png', 'wb') as f:
        f.write(base64.b64decode(qr_img))
    image = Image.open('code.png')
    image.show()
    print("请扫描验证码! ")


def wss_on_message(ws, message):
    print("=============== [message] ===============")
    message = json.loads(message)
    print(message)
    if "扫码成功" in message["msg"]:
        global once_password, uuid
        once_password = message["oncePassword"]
        uuid = message["uuid"]
        ws.close()


def wss_on_error(ws, error):
    print("=============== [error] ===============")
    print(error)
    ws.close()


def wss_on_close(ws, close_status_code, close_msg):
    print("=============== [closed] ===============")
    print(close_status_code)
    print(close_msg)


def wss_on_open(ws):
    def run(*args):
        while True:
            ws.send(qr_token)
            time.sleep(8)
    _thread.start_new_thread(run, (qr_token,))


def wss():
    # websocket.enableTrace(True)  # 是否显示连接详细信息
    ws = websocket.WebSocketApp(
        web_socket_url % qr_token, on_open=wss_on_open,
        on_message=wss_on_message, on_error=wss_on_error,
        on_close=wss_on_close
    )
    ws.run_forever()


def get_cookie_second():
    global cookie
    params = {
        "pwd": once_password,
        "service": "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
    }
    headers["Host"] = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
    headers["Referer"] = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
    response = requests.get(url=login_url, params=params, headers=headers, cookies=cookie, allow_redirects=False)
    cookie.update(response.cookies.get_dict())
    location = response.headers.get("Location")
    return location


def get_cookie_third(location):
    global cookie
    headers["Host"] = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
    headers["Referer"] = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
    response = requests.get(url=location, headers=headers, cookies=cookie, allow_redirects=False)
    cookie.update(response.cookies.get_dict())
    location = response.headers.get("Location")
    return location


def get_login_user_info():
    headers["Host"] = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
    headers["Origin"] = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
    headers["Referer"] = "脱敏处理,完整代码关注 GitHub:https://github.com/kgepachong/crawler"
    params = {"time": str(int(time.time() * 1000))}
    response = requests.get(url=user_info_url, headers=headers, cookies=cookie, params=params)
    print(response.text)


def main():
    # 第一次获取 cookie,包含 INGRESSCOOKIE、JSESSIONID、SERVERID、acw_tc
    get_cookies_first()
    # 获取二维码
    get_login_qr_img()
    # websocket 扫码登录,返回一次性密码
    wss()
    # 第二次获取 cookie,更新 SERVERID、获取 CASLOGC、CASTGC
    location1 = get_cookie_second()
    # 第三次获取 cookie,获取 SESSION
    get_cookie_third(location1)
    # 获取登录用户信息
    get_login_user_info()


if __name__ == '__main__':
    main()

pip install aiohttp

aiohttp 是一个用于 Python 的异步 HTTP 客户端/服务器框架,支持 WebSocket 连接

aiohttp WebSocket 客户端示例

import aiohttp
import asyncio

async def websocket_client():
    async with aiohttp.ClientSession() as session:
        async with session.ws_connect('ws://websockets.chilkat.io/wsChilkatEcho.ashx') as ws:
            await ws.send_str('Hello, Server')
            async for msg in ws:
                if msg.type == aiohttp.WSMsgType.TEXT:
                    print(f'Received: {msg.data}')
                    break
                elif msg.type == aiohttp.WSMsgType.CLOSED:
                    break
                elif msg.type == aiohttp.WSMsgType.ERROR:
                    break

# 运行异步函数
asyncio.run(websocket_client())

aiohttp WebSocket 服务器示例

from aiohttp import web

async def websocket_handler(request):
    ws = web.WebSocketResponse()
    await ws.prepare(request)

    async for msg in ws:
        if msg.type == aiohttp.WSMsgType.TEXT:
            await ws.send_str(f'Echo: {msg.data}')
        elif msg.type == aiohttp.WSMsgType.BINARY:
            await ws.send_bytes(msg.data)
        elif msg.type == aiohttp.WSMsgType.CLOSE:
            await ws.close()
            break

    return ws

app = web.Application()
app.add_routes([web.get('/ws', websocket_handler)])
web.run_app(app, port=8080)

### 使用 Python 及工具下载图片音乐视频 #### 下载图片的方法 可以使用 `requests` 库来实现图片下载功能。以下是具体代码示例: ```python import requests def download_image(url, save_path): response = requests.get(url, stream=True) if response.status_code == 200: with open(save_path, 'wb') as file: for chunk in response.iter_content(1024): file.write(chunk) # 调用函数 image_url = "https://example.com/sample.jpg" download_image(image_url, "./sample.jpg") # 将图片保存到当前目录下的 sample.jpg 文件中[^3] ``` 此方法通过发送 HTTP 请求获取资源数据流并将其写入本地文件。 --- #### 下载音乐的方法 对于音频文件,同样可利用 `requests` 或者更高级别的命令行工具如 `wget` 和 `pycurl` 来完成操作。下面分别展示两种方式: ##### 方法一:基于 Requests 的实现 ```python def download_music(url, save_path): response = requests.get(url, stream=True) if response.status_code == 200: with open(save_path, 'wb') as file: for chunk in response.iter_content(1024 * 8): # 增大缓冲区大小提高效率 file.write(chunk) music_url = "http://example.com/song.mp3" download_music(music_url, "./song.mp3") # 将歌曲保存至 song.mp3 中[^4] ``` ##### 方法二:基于 Wget 实现 如果偏好于调用外部程序,则可以通过子进程模块执行 wget 命令: ```bash !wget http://example.com/song.mp3 -O ./song.mp3 ``` 或者在 Python 中运行该指令: ```python import subprocess subprocess.run(["wget", "-q", "--show-progress", music_url, "-O", "./song.mp3"]) # 静默模式下显示进度条[^5] ``` --- #### 下载视频的方法 针对在线平台上的多媒体内容提取需求,推荐采用专门设计用于此类场景的应用软件——You-GetFFmpeg 结合的方式处理复杂情况;而对于简单链接则依旧适用前述技术栈。 ##### You-Get 工具简介及其应用实例 You-Get 是一款开源项目,支持从主流网站抓取音视频素材。安装完成后即可直接输入目标地址进行解析与存储动作。 ```bash $ you-get https://www.youtube.com/watch?v=dQw4w9WgXcQ --output-filename="RickRoll.mp4" ``` 对应嵌套进脚本内的形式如下所示: ```python from you_get import common as you_get_common url = "https://www.bilibili.com/video/BV1xx411c7mD/" options = ["--debug"] # 添加调试参数便于排查错误信息 you_get_common.any_download(url=url, info_only=False, output_dir="./downloads/", merge=True, **{"extra": options}) ``` ##### 利用 PyCURL 替代部分场合中的网络交互逻辑 当追求极致性能优化时,PyCURL 提供了一个高性能替代方案。它允许开发者细粒度控制传输过程中的各个环节。 ```python import pycurl from io import BytesIO buffer = BytesIO() crul_instance = pycurl.Curl() video_link = "http://techslides.com/demos/sample-videos/small.mp4" with crul_instance as curl_obj: curl_obj.setopt(curl_obj.URL, video_link) curl_obj.setopt(curl_obj.WRITEDATA, buffer) curl_obj.perform() # 执行请求并将结果存放到内存对象里 data_stream = buffer.getvalue() with open("./small_video.mp4", mode='wb') as vid_file_handler: vid_file_handler.write(data_stream) # 把接收到的数据转储成实际磁盘文件[^6] ``` 最后提到的是关于后期编辑合成阶段需要用到的功能强大的跨平台多媒体框架 ——FFMPEG 。虽然其本身并非纯 python 组件 ,但借助封装好的接口类库比如 moviepy , av 等能够轻松集成进来满足更多定制化诉求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值