requests支持以下新特性:
- 支持http2,默认以http2进行连接,连接失败后会进行退化而进行http1.1连接
- 支持JA3指纹修改
- 支持http2+JA3指纹
- 支持在使用代理的基础上修改JA3指纹
此模块参考于Python的requests模块
go get github.com/wangluozhe/requests
迫不及待了吗?本页内容为如何入门 Requests 提供了很好的指引。其假设你已经安装了 Requests。如果还没有,去安装看看吧。
首先,确认一下:
- Requests已安装
- Requests是最新的
让我们从一些简单的示例开始吧。
使用 requests 发送网络请求非常简单。
一开始要导入requests 模块:
import (
"github.com/wangluozhe/requests"
"github.com/wangluozhe/requests/url"
)
然后,尝试获取某个网页。本例子中,我们来获取 Github 的公共时间线:
r, err := requests.Get('https://api.github.com/events', nil)
现在,我们有一个名为 r
的 Response
对象。我们可以从这个对象中获取所有我们想要的信息。
Requests 简便的 API 意味着所有 HTTP 请求类型都是显而易见的。例如,你可以这样发送一个 HTTP POST 请求:
data := url.NewData()
data.Set("key","value")
r, err := requests.Post("http://httpbin.org/post", &url.Request{Data: data})
漂亮,对吧?那么其他 HTTP 请求类型:PUT,DELETE,HEAD 以及 OPTIONS 又是如何的呢?都是一样的简单:
data := url.NewData()
data.Set("key","value")
r, err := requests.Post("http://httpbin.org/post", &url.Request{Data: data})
r := requests.Delete('http://httpbin.org/delete')
r := requests.Head('http://httpbin.org/get')
r := requests.Options('http://httpbin.org/get')
都很不错吧,但这也仅是 requests 的冰山一角呢。
你也许经常想为 URL 的查询字符串(query string)传递某种数据。如果你是手工构建 URL,那么数据会以键/值对的形式置于 URL 中,跟在一个问号的后面。例如, httpbin.org/get?key=val
。 Requests 允许你使用 params
关键字参数,以一个字符串字典来提供这些参数。举例来说,如果你想传递 key1=value1
和 key2=value2
到 httpbin.org/get
,那么你可以使用如下代码:
params := url.NewParams()
params.Set("key1","value1")
params.Set("key2","value2")
r, err := requests.Get("http://httpbin.org/get",&url.Request{Params: params})
通过打印输出该 URL,你能看到 URL 已被正确编码:
fmt.Println(r.Url)
http://httpbin.org/get?key1=value1&key2=value2
你还可以使Params有多个值传入:
params := url.NewParams()
params.Set("key1","value1")
params.Add("key1","value2")
params.Set("key2","value2")
r, err := requests.Get("http://httpbin.org/get",&url.Request{Params: params})
fmt.Println(r.Url)
http://httpbin.org/post?key1=value1&key1=value2&key2=value2
我们能读取服务器响应的内容。再次以 GitHub 时间线为例:
package main
import "github.com/wangluozhe/requests"
func main(){
r, err := requests.Get('https://api.github.com/events', nil)
fmt.Println(r.Text)
}
[{"repository":{"open_issues":0,"url":"https://github.com/...
requests 会自动解码来自服务器的内容。大多数 unicode 字符集都能被无缝地解码。
你也能以字节数组的方式访问请求响应体,对于非文本请求:
fmt.Println(r.Content)
[91 123 34 105 100 34 58 34 50 48 55 49 52 50 51 57 56 48 53 34 44 34 116 121 112 101 34 58 34...
Requests 会自动为你解码 gzip
和 deflate
以及br
传输编码的响应数据。
例如,以请求返回的二进制数据创建一张图片,你可以使用如下代码:
package main
import "github.com/wangluozhe/requests"
func main(){
r, err := requests.Get("图片URL", nil)
if err != nil {
fmt.Println(err)
}
jpg,_ := os.Create("1.jpg")
io.Copy(jpg,resp.Body) // 第一种
//jpg.Write(resp.Content) // 第二种
}
Requests 中也有一个内置的 JSON 解码器,助你处理 JSON 数据:
package main
import "github.com/wangluozhe/requests"
func main(){
r, err := requests.Get('https://api.github.com/events', nil)
json, err := r.Json()
fmt.Println(json, err)
}
{"Accept-Ranges":["bytes"],"Access-Control-Allow-Origin":["*"],"Access-Control...
如果 JSON 解码失败, r.Json()
就会返回一个异常。例如,响应内容是 401 (Unauthorized),尝试访问 r.Json()
将会抛出 map[] invalid character '(' after top-level value
异常。
需要注意的是,成功调用 r.Json()
并不意味着响应的成功。有的服务器会在失败的响应中包含一个 JSON 对象(比如 HTTP 500 的错误细节)。这种 JSON 会被解码返回。要检查请求是否成功,请检查 r.StatusCode
是否和你的期望相同。
在罕见的情况下,你可能想获取来自服务器的原始套接字响应,那么你可以访问 r.Body
。 具体你可以这么做:
r, err := requests.Get("http://www.baidu.com", nil)
if err != nil {
fmt.Println(err)
}
fmt.Println(resp.Body)
// 返回的是io.ReadCloser类型
但一般情况下,你应该以下面的模式将文本流保存到文件:
f,_ := os.Create("baidu.txt")
io.Copy(f,resp.Body)
如果你想为请求添加 HTTP 头部,只要简单地url.NewHeaders()
给 Headers
参数就可以了。
例如,在前一个示例中我们没有指定 content-type:
url := "https://api.github.com/some/endpoint"
headers := url.NewHeaders()
headers.Set("user-agent", "my-app/0.0.1")
req := url.NewRequest()
req.Headers = headers
r := requests.Get(url, req)
注意: 定制 header 的优先级低于某些特定的信息源,例如:
Content-Length
请求头不能随便设置,可能会有错误发生。
- 如果被重定向到别的主机,授权 header 就会被删除。
- 代理授权 header 会被 URL 中提供的代理身份覆盖掉。
- 在我们能判断内容长度的情况下,header 的 Content-Length 会被改写。
更进一步讲,Requests 不会基于定制 header 的具体情况改变自己的行为。只不过在最后的请求中,所有的 header 信息都会被传递进去。
注意: 所有的 header 值必须是 string
。
如果你想让你的请求头变为有序的话,请添加一个"Header-Order:"
参数即可,值为有序请求头数组,注:值必须全部为小写。
例如:
package main
import (
"github.com/wangluozhe/requests/url"
)
func main(){
headers := url.NewHeaders()
headers.Set("Path", "/get")
headers.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36")
headers.Set("accept-language", "zh-CN,zh;q=0.9")
headers.Set("Scheme", "https")
headers.Set("accept-encoding", "gzip, deflate, br")
//headers.Set("Content-Length", "100") // 注意,不能随便改变Content-Length大小
headers.Set("Host", "httpbin.org")
headers.Set("Accept", "application/json, text/javascript, */*; q=0.01")
(*headers)["Header-Order:"] = []string{ // 请求头排序,值必须为小写
"user-agent",
"path",
"accept-language",
"scheme",
"connection",
"accept-encoding",
"content-length",
"host",
"accept",
}
}
req.Headers = headers
r, err := requests.Get("https://httpbin.org/get", req)
if err != nil {
fmt.Println(err)
}
fmt.Println("text:", r.Text)
// 最好用fiddler抓包工具查看一下
通常,你想要发送一些编码为表单形式的数据——非常像一个 HTML 表单。要实现这个,只需简单地url.NewData()
给 Data
参数。你的数据字典在发出请求时会自动编码为表单形式:
data := url.NewData()
data.Set("key1","value1")
data.Set("key2","value2")
req := url.NewRequest()
req.Data = data
r, err := requests.Post("http://www.baidu.com",req)
if err != nil {
fmt.Println(err)
}
fmt.Println(resp.Text)
...
"form": {
"key1": "value1",
"key2": "value2"
}
...
你还可以为 data
在表单中多个元素使用同一 key 的时候,这种方式尤其有效:
data := url.NewData()
data.Set("key1","value1")
data.Add("key1","value3")
data.Set("key2","value2")
data.Add("key2","value4")
req := url.NewRequest()
req.Data = data
r, err := requests.Post("http://httpbin.org/post",req)
if err != nil {
fmt.Println(err)
}
fmt.Println(resp.Text)
...
"form": {
"key1": [
"value1",
"value3"
],
"key2": [
"value2",
"value4"
]
}
...
很多时候你想要发送的数据并非编码为表单形式的。如果你想传递一个 json
数据那么用下面的方法。
你可以使用 Json
参数直接传递,然后它就会被自动编码。
req := url.NewRequest()
req.Json = map[string]string{"some":"data"}
r, err := requests.Post("http://httpbin.org/post",req)
if err != nil {
fmt.Println(err)
}
fmt.Println(resp.Text)
...
"json": {
"some": "data"
}
...
requests 使得上传多部分编码文件变得很简单:
files := url2.NewFiles()
// SetFile(name,fileName,filePath,contentType)
// name为字段名,fileName为上传的文件名,filePath为上传文件的绝对路径,contentType为上传的文件类型
// 如果contentType设置为"",则默认为"application/octet-stream"
files.SetFile("api","api","D:\\Go\\github.com\\wangluozhe\\requests\\api.go","")
req := url2.NewRequest()
req.Files = files
r, err := requests.Post("http://httpbin.org/post",req)
if err != nil {
fmt.Println(err)
}
fmt.Println(r.Text)
...
"files": {
"api": "文件内容"
}
...
requests使得FormData的使用也方便多了:
files := url2.NewFiles()
// SetFile(name,value)
// name为字段名,value为值
files.SetField("name","value")
req := url2.NewRequest()
req.Files = files
r, err := requests.Post("http://httpbin.org/post",req)
if err != nil {
fmt.Println(err)
}
fmt.Println(resp.Text)
...
"form": {
"name": "value"
}
...
我们可以检测响应状态码:
r, err := requests.Get("http://httpbin.org/get", nil)
fmt.Println(r.StatusCode)
200
为方便引用,可以直接使用此方法:
fmt.Println(r.StatusCode == http.StatusOK)
True
如果发送了一个错误请求(一个 4XX 客户端错误,或者 5XX 服务器错误响应),我们可以通过 Response.RaiseForStatus()
来抛出异常:
r, err := requests.Get('http://httpbin.org/status/404', nil)
fmt.Println(r.StatusCode)
404
fmt.Println(r.RaiseForStatus())
404 Client Error
但是,由于我们的例子中 r
的 StatusCode
是 200
,当我们调用 RaiseForStatus()
时,得到的是:
fmt.Println(r.RaiseForStatus())
nil
一切都挺和谐哈。
我们可以查看以一个 http.Header形式(实际是一个map[string][]string类型)展示的服务器响应头:
fmt.Println(r.Headers)
map[Access-Control-Allow-Credentials:[true] Access-Control-Allow-Origin:[*] Connection:[keep-alive] Content-Length:[1976] Content-Type:[application/json] Date:[Sat, 12 Mar 2022 15:59:05 GMT] Server:[gunicorn/19.9.0]]
但是这个类型比较特殊:它是仅为 HTTP 头部而生的。根据 RFC 2616, HTTP 头部是大小写不敏感的。
因此,我们可以使用任意大写形式来访问这些响应头字段:
fmt.Println(r.Headers["Content-Type"][0])
application/json
fmt.Println(r.Headers.Get('content-type'))
application/json
它还有一个特殊点,那就是服务器可以多次接受同一 header,每次都使用不同的值。但 Requests 会将它们合并,这样它们就可以用一个映射来表示出来,参见 RFC 7230:
A recipient MAY combine multiple header fields with the same field name into one "field-name: field-value" pair, without changing the semantics of the message, by appending each subsequent field value to the combined field value in order, separated by a comma.
接收者可以合并多个相同名称的 header 栏位,把它们合为一个 "field-name: field-value" 配对,将每个后续的栏位值依次追加到合并的栏位值中,用逗号隔开即可,这样做不会改变信息的语义。
如果某个响应中包含一些 cookie,你可以快速访问它们:
url := "https://www.baidu.com"
r, err := requests.Get(url, nil)
fmt.Println(r.Cookies)
[BD_NOT_HTTPS=1; Path=/; Max-Age=300 BIDUPSID=7DB59A8D47E763943295969C33979837; Path=/; Domain=baidu.com; Max-Age=2147483647 PSTM=1647233990; Path=/; Domain=baidu.com; Max-Age=2147483647 BAIDUID=7DB59A8D47E7639495833AF6370F9985:FG=1; Path=/; Domain=baidu.com; Max-Age=31536000]
要想发送你的cookies到服务器,可以使用 cookies
参数:
req := url.NewRequest()
cookies,_ := cookiejar.New(nil)
urls, _ := url.Parse("http://httpbin.org/cookies")
cookies.SetCookies(urls,[]*http.Cookie{&http.Cookie{
Name: "cookies_are",
Value: "working",
}})
req.Cookies = cookies
r, err := requests.Get("http://httpbin.org/cookies", req)
if err != nil {
fmt.Println(err)
}
fmt.Println(r.Text)
{"cookies": {"cookies_are": "working"}}
默认情况下,除了 HEAD, Requests 会自动处理所有重定向。
可以使用响应对象的 history
方法来追踪重定向。
Response.History
是一个 Response
对象的列表,为了完成请求而创建了这些对象。这个对象列表按照从最老到最近的请求进行排序。
例如,Github 将所有的 HTTP 请求重定向到 HTTPS:
r, err := requests.Get("http://github.com", nil)
fmt.Println(r.Url)
https://github.com/
fmt.Println(r.StatusCode)
200
fmt.Println(r.History)
[0xc0001803f0]
如果你使用的是GET、OPTIONS、POST、PUT、PATCH 或者 DELETE,那么你可以通过 allow_redirects
参数禁用重定向处理:
req := url.NewRequest()
req.AllowRedirects = false
r, err := requests.Get('http://github.com', req)
fmt.Println(r.StatusCode)
301
fmt.Println(r.History)
[]
如果你使用了 HEAD,你也可以启用重定向:
req := url.NewRequest()
req.AllowRedirects = true
r, err := requests.Get('http://github.com', req)
fmt.Println(r.Url)
https://github.com/
fmt.Println(r.History)
[0xc0001803f0]
你可以告诉 requests 在经过以 Timeout
参数设定的秒数时间之后停止等待响应。基本上所有的生产代码都应该使用这一参数。如果不使用,你的程序可能会永远失去响应:
req := url2.NewRequest()
req.Timeout = 1 * time.Millisecond
r, err := requests.Get("http://github.com",req)
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x253460]
goroutine 1 [running]:
main.main()
D:/Go/github.com/wangluozhe/requests/examples/test.go:27 +0xc0
注意
Timeout
仅对连接过程有效,与响应体的下载无关。 Timeout
并不是整个下载响应的时间限制,而是如果服务器在 Timeout
秒内没有应答,将会引发一个异常(更精确地说,是在 Timeout
秒内没有从基础套接字上接收到任何字节的数据时)If no timeout is specified explicitly, requests do not time out.
许多要求身份认证的web服务都接受 HTTP Basic Auth。这是最简单的一种身份认证,并且 requests 对这种认证方式的支持是直接开箱即可用。
以 HTTP Basic Auth 发送请求非常简单:
package main
import (
"fmt"
"github.com/wangluozhe/requests"
"github.com/wangluozhe/requests/url"
)
func main() {
req := url.NewRequest()
req.Auth = []string{"user","password"}
r, err := requests.Get("http://httpbin.org/basic-auth/user/password",req)
if err != nil{
fmt.Println(err)
}
fmt.Println("text:",r.Text)
}
{
"authenticated": true,
"user": "user"
}
你也可以指定一个本地证书用作客户端证书,可以是一个包含两个文件路径的数组(cert,key)或一个包含三个文件路径的数组(cert,key,根证书):
req := url.NewRequest()
req.Cert = []string{"cert","key"}
// req.Cert = []string{"cert","key","rootca"}
r, err := requests.Get("xxx",req)
if err != nil{
fmt.Println(err)
}
或者保持在会话中:
session := requests.NewSession()
session.Cert = []string{"cert","key"}
requests也支持JA3指纹的修改,可以让你在访问的时候使用你自己定义的JA3指纹进行TLS握手访问,但是请注意,JA3指纹必须符合要求,不能随便更改,最好使用wireshark或者ja3er.com获取标准指纹,而不是随便输入一串数字。
req := url.NewRequest()
headers := url.NewHeaders()
headers.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36")
req.Headers = headers
req.Ja3 = "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-21,29-23-24,0"
r, err := requests.Get("https://ja3er.com/json", req)
if err != nil {
fmt.Println(err)
}
fmt.Println(r.Text)
{"ja3_hash":"b32309a26951912be7dba376398abc3b", "ja3": "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-21,29-23-24,0", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"}