12.3 HTTP编程
12.3.1 概述
12.3.1.1 Web工作方式
我们平时浏览网页的时候,会打开浏览器,输入网址后按下回车键,然后就会显示出你想要浏览的内容。在这个看似简单的用户行为背后,到底隐藏了些什么呢?
对于普通的上网过程,系统其实是这样做的:浏览器本身是一个客户端,当你输入URL的时候,首先浏览器会去请求DNS服务器,通过DNS获取相应的域名对应的IP,然后通过IP地址找到IP对应的服务器后,要求建立TCP连接,等浏览器发送完HTTP Request(请求)包后,服务器接收到请求包之后才开始处理请求包,服务器调用自身服务,返回HTTP Response(响应)包;客户端收到来自服务器的响应后开始渲染这个Response包里的主体(body),等收到全部的内容随后断开与该服务器之间的TCP连接。
一个Web服务器也被称为HTTP服务器,它通过HTTP协议与客户端通信。这个客户端通常指的是Web浏览器(其实手机端客户端内部也是浏览器实现的)。
Web服务器的工作原理可以简单地归纳为:
客户机通过TCP/IP协议建立到服务器的TCP连接
客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档
服务器向客户机发送HTTP协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端
客户机与服务器断开。由客户端解释HTML文档,在客户端屏幕上渲染图形结果
12.3.1.2 HTTP协议
超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议,它详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。
HTTP协议通常承载于TCP协议之上,有时也承载于TLS或SSL协议层之上,这个时候,就成了我们常说的HTTPS。如下图所示:
12.3.1.3 地址(URL)
URL全称为Unique Resource Location,用来表示网络资源,可以理解为网络文件路径。
URL的格式如下:
http://host[":"port][abs_path]
http://192.168.31.1/html/index
URL的长度有限制,不同的服务器的限制值不太相同,但是不能无限长。
12.3.2 HTTP报文浅析
12.3.2.1 请求报文格式
- 测试代码
服务器测试代码:
package main
import (
"fmt"
"log"
"net"
)
func main() {
//创建、监听socket
listenner, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
log.Fatal(err) //log.Fatal()会产生panic
}
defer listenner.Close()
conn, err := listenner.Accept() //阻塞等待客户端连接
if err != nil {
log.Println(err)
return
}
defer conn.Close() //此函数结束时,关闭连接套接字
//conn.RemoteAddr().String():连接客服端的网络地址
ipAddr := conn.RemoteAddr().String()
fmt.Println(ipAddr, "连接成功")
buf := make([]byte, 4096) //缓冲区,用于接收客户端发送的数据
//阻塞等待用户发送的数据
n, err := conn.Read(buf) //n代码接收数据的长度
if err != nil {
fmt.Println(err)
return
}
//切片截取,只截取有效数据
result := buf[:n]
fmt.Printf("接收到数据来自[%s]==>:\n%s\n", ipAddr, string(result))
}
浏览器输入url地址:
服务器端运行打印结果如下:
- 请求报文格式说明
HTTP 请求报文由请求行、请求头部、空行、请求包体4个部分组成,如下图所示: - 请求行
请求行由方法字段、URL 字段 和HTTP 协议版本字段 3 个部分组成,他们之间使用空格隔开。常用的 HTTP 请求方法有 GET、POST。
GET:
当客户端要从服务器中读取某个资源时,使用GET 方法。GET 方法要求服务器将URL 定位的资源放在响应报文的数据部分,回送给客户端,即向服务器请求某个资源。
使用GET方法时,请求参数和对应的值附加在 URL 后面,利用一个问号(“?”)代表URL 的结尾与请求参数的开始,传递参数长度受限制,因此GET方法不适合用于上传数据。
通过GET方法来获取网页时,参数会显示在浏览器地址栏上,因此保密性很差。
POST:
当客户端给服务器提供信息较多时可以使用POST 方法,POST 方法向服务器提交数据,比如完成表单数据的提交,将数据提交给服务器处理。
GET 一般用于获取/查询资源信息,POST 会附带用户数据,一般用于更新资源信息。POST 方法将请求参数封装在HTTP 请求数据中,而且长度没有限制,因为POST携带的数据,在HTTP的请求正文中,以名称/值的形式出现,可以传输大量数据。
- 请求头部
请求头部为请求报文添加了一些附加信息,由“名/值”对组成,每行一对,名和值之间使用冒号分隔。
请求头部通知服务器有关于客户端请求的信息,典型的请求头有:
请求头 | 含义 |
User-Agent | 请求的浏览器类型 |
Accept | 客户端可识别的响应内容类型列表,星号“ * ”用于按范围将类型分组,用“ / ”指示可接受全部类型,用“ type/* ”指示可接受 type 类型的所有子类型 |
Accept-Language | 客户端可接受的自然语言 |
Accept-Encoding | 客户端可接受的编码压缩格式 |
Accept-Charset | 可接受的应答的字符集 |
Host | 请求的主机名,允许多个域名同处一个IP 地址,即虚拟主机 |
connection | 连接方式(close或keepalive) |
Cookie | 存储于客户端扩展字段,向同一域名的服务端发送属于该域的cookie |
- 空行
最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头。 - 请求包体
请求包体不在GET方法中使用,而是POST方法中使用。
POST方法适用于需要客户填写表单的场合。与请求包体相关的最常使用的是包体类型Content-Type和包体长度Content-Length。
12.3.2.2 响应报文格式
- 测试代码
服务器示例代码:
package main
import (
"fmt"
"net/http"
)
//服务端编写的业务逻辑处理程序
func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello world")
}
func main() {
http.HandleFunc("/go", myHandler)
//在指定的地址进行监听,开启一个HTTP
http.ListenAndServe("127.0.0.1:8000", nil)
}
启动服务器程序:
客户端测试示例代码:
package main
import (
"fmt"
"log"
"net"
)
func main() {
//客户端主动连接服务器
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
log.Fatal(err) //log.Fatal()会产生panic
return
}
defer conn.Close() //关闭
requestHeader := "GET /go HTTP/1.1\r\nAccept: image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/xaml+xml, application/x-ms-xbap, */*\r\nAccept-Language: zh-Hans-CN,zh-Hans;q=0.8,en-US;q=0.5,en;q=0.3\r\nUser-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729)\r\nAccept-Encoding: gzip, deflate\r\nHost: 127.0.0.1:8000\r\nConnection: Keep-Alive\r\n\r\n"
//先发送请求包
conn.Write([]byte(requestHeader))
buf := make([]byte, 4096) //缓冲区
//阻塞等待服务器回复的数据
n, err := conn.Read(buf) //n代码接收数据的长度
if err != nil {
fmt.Println(err)
return
}
//切片截取,只截取有效数据
result := buf[:n]
fmt.Printf("接收到数据[%d]:\n%s\n", n, string(result))
}
启动程序,测试http的成功响应报文:
启动程序,测试http的失败响应报文:
- 响应报文格式说明
HTTP 响应报文由状态行、响应头部、空行、响应包体4个部分组成,如下图所示: - 状态行
状态行由 HTTP 协议版本字段、状态码和状态码的描述文本3个部分组成,他们之间使用空格隔开。
状态码:
状态码由三位数字组成,第一位数字表示响应的类型,常用的状态码有五大类如下所示:
状态码 含义
1xx 表示服务器已接收了客户端请求,客户端可继续发送请求
2xx 表示服务器已成功接收到请求并进行处理
3xx 表示服务器要求客户端重定向
4xx 表示客户端的请求有非法内容
5xx 表示服务器未能正常处理客户端的请求而出现意外错误
常见的状态码举例:
状态码 含义
200 OK 客户端请求成功
400 Bad Request 请求报文有语法错误
401 Unauthorized 未授权
403 Forbidden 服务器拒绝服务
404 Not Found 请求的资源不存在
500 Internal Server Error 服务器内部错误
503 Server Unavailable 服务器临时不能处理客户端请求(稍后可能可以)
- 响应头部
响应头可能包括:
响应头 含义
Location Location响应报头域用于重定向接受者到一个新的位置
Server Server 响应报头域包含了服务器用来处理请求的软件信息及其版本
Vary 指示不可缓存的请求头列表
Connection 连接方式 - 空行
最后一个响应头部之后是一个空行,发送回车符和换行符,通知服务器以下不再有响应头部。 - 响应包体
服务器返回给客户端的文本信息。
12.3.3 HTTP编程
Go语言标准库内建提供了net/http包,涵盖了HTTP客户端和服务端的具体实现。使用
net/http包,我们可以很方便地编写HTTP客户端或服务端的程序。
12.3.3.1 HTTP服务端
示例代码:
package main
import (
"fmt"
"net/http"
)
//服务端编写的业务逻辑处理程序
//hander函数: 具有func(w http.ResponseWriter, r *http.Requests)签名的函数
func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.RemoteAddr, "连接成功") //r.RemoteAddr远程网络地址
fmt.Println("method = ", r.Method) //请求方法
fmt.Println("url = ", r.URL.Path)
fmt.Println("header = ", r.Header)
fmt.Println("body = ", r.Body)
w.Write([]byte("hello go")) //给客户端回复数据
}
func main() {
http.HandleFunc("/go", myHandler)
//该方法用于在指定的 TCP 网络地址 addr 进行监听,然后调用服务端处理程序来处理传入的连接请求。
//该方法有两个参数:第一个参数 addr 即监听地址;第二个参数表示服务端处理程序,通常为空
//第二个参数为空意味着服务端调用 http.DefaultServeMux 进行处理
http.ListenAndServe("127.0.0.1:8000", nil)
}
浏览器输入url地址:
服务器运行结果:
12.3.3.2 HTTP客户端
package main
import (
"fmt"
"io"
"log"
"net/http"
)
func main() {
//get方式请求一个资源
//resp, err := http.Get("http://www.baidu.com")
//resp, err := http.Get("http://www.neihan8.com/article/index.html")
resp, err := http.Get("http://127.0.0.1:8000/go")
if err != nil {
log.Println(err)
return
}
defer resp.Body.Close() //关闭
fmt.Println("header = ", resp.Header)
fmt.Printf("resp status %s\nstatusCode %d\n", resp.Status, resp.StatusCode)
fmt.Printf("body type = %T\n", resp.Body)
buf := make([]byte, 2048) //切片缓冲区
var tmp string
for {
n, err := resp.Body.Read(buf) //读取body包内容
if err != nil && err != io.EOF {
fmt.Println(err)
return
}
if n == 0 {
fmt.Println("读取内容结束")
break
}
tmp += string(buf[:n]) //累加读取的内容
}
fmt.Println("buf = ", string(tmp))
}
示例代码,百度贴吧爬虫
package main
import (
"fmt"
"net/http"
"os"
"strconv"
)
//爬取网页内容
func HttpGet(url string) (result string, err error) {
resp, err1 := http.Get(url)
if err1 != nil {
err = err1
return
}
defer resp.Body.Close()
//读取网页body内容
buf := make([]byte, 1024*4)
for {
n, _ := resp.Body.Read(buf)
if n == 0 { //读取结束,或者,出问题
//fmt.Println("resp.Body.Read err = ", err)
break
}
result += string(buf[:n])
}
return
}
func DoWork(start, end int) {
fmt.Printf("正在爬取 %d 到 %d 的页面\n", start, end)
//明确目标 (要知道你准备在哪个范围或者网站去搜索)
//http://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=0 //下一页+50
for i := start; i <= end; i++ {
url := "http://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=" + strconv.Itoa((i-1)*50)
fmt.Println("url = ", url)
//2) 爬 (将所有的网站的内容全部爬下来)
result, err := HttpGet(url)
if err != nil {
fmt.Println("HttpGet err = ", err)
continue
}
//把内容写入到文件
fileName := strconv.Itoa(i) + ".html"
f, err1 := os.Create(fileName)
if err1 != nil {
fmt.Println("os.Create err1 = ", err1)
continue
}
f.WriteString(result) //写内容
f.Close() //关闭文件
}
}
func main() {
var start, end int
fmt.Printf("请输入起始页( >= 1) :")
fmt.Scan(&start)
fmt.Printf("请输入终止页( >= 起始页) :")
fmt.Scan(&end)
DoWork(start, end)
}
并发版爬虫代码
package main
import (
"fmt"
"net/http"
"os"
"strconv"
)
//爬取网页内容
func HttpGet(url string) (result string, err error) {
resp, err1 := http.Get(url)
if err1 != nil {
err = err1
return
}
defer resp.Body.Close()
//读取网页body内容
buf := make([]byte, 1024*4)
for {
n, _ := resp.Body.Read(buf)
if n == 0 { //读取结束,或者,出问题
//fmt.Println("resp.Body.Read err = ", err)
break
}
result += string(buf[:n])
}
return
}
//爬取一个网页
func SpiderPape(i int, page chan<- int) {
url := "http://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=" + strconv.Itoa((i-1)*50)
fmt.Printf("正在爬第%d页网页: %s\n", i, url)
//2) 爬 (将所有的网站的内容全部爬下来)
result, err := HttpGet(url)
if err != nil {
fmt.Println("HttpGet err = ", err)
return
}
//把内容写入到文件
fileName := strconv.Itoa(i) + ".html"
f, err1 := os.Create(fileName)
if err1 != nil {
fmt.Println("os.Create err1 = ", err1)
return
}
f.WriteString(result) //写内容
f.Close() //关闭文件
page <- i
}
func DoWork(start, end int) {
fmt.Printf("正在爬取 %d 到 %d 的页面\n", start, end)
page := make(chan int)
//明确目标 (要知道你准备在哪个范围或者网站去搜索)
//http://tieba.baidu.com/f?kw=%E7%BB%9D%E5%9C%B0%E6%B1%82%E7%94%9F&ie=utf-8&pn=0 //下一页+50
for i := start; i <= end; i++ {
go SpiderPape(i, page)
}
for i := start; i <= end; i++ {
fmt.Printf("第%d个页面爬取完成\n", <-page)
}
}
func main() {
var start, end int
fmt.Printf("请输入起始页( >= 1) :")
fmt.Scan(&start)
fmt.Printf("请输入终止页( >= 起始页) :")
fmt.Scan(&end)
DoWork(start, end)
}
示例代码,段子网站爬虫
package main
import (
"fmt"
"net/http"
"os"
"regexp"
"strconv"
"strings"
)
func HttpGet(url string) (result string, err error) {
resp, err1 := http.Get(url) //发送get请求
if err1 != nil {
err = err1
return
}
defer resp.Body.Close()
//读取网页内容
buf := make([]byte, 4*1024)
for {
n, _ := resp.Body.Read(buf)
if n == 0 {
break
}
result += string(buf[:n]) //累加读取的内容
}
return
}
//开始爬取每一个笑话,每一个段子 title, content, err := SpiderOneJoy(url)
func SpiderOneJoy(url string) (title, content string, err error) {
//开始爬取页面内容
result, err1 := HttpGet(url)
if err1 != nil {
//fmt.Println("HttpGet err = ", err)
err = err1
return
}
//取关键信息
//取标题 <h1> 标题 </h1> 只取1个
re1 := regexp.MustCompile(`<h1>(?s:(.*?))</h1>`)
if re1 == nil {
//fmt.Println("regexp.MustCompile err")
err = fmt.Errorf("%s", "regexp.MustCompile err")
return
}
//取内容
tmpTitle := re1.FindAllStringSubmatch(result, 1) //最后一个参数为1,只过滤第一个
for _, data := range tmpTitle {
title = data[1]
// title = strings.Replace(title, "\r", "", -1)
// title = strings.Replace(title, "\n", "", -1)
// title = strings.Replace(title, " ", "", -1)
title = strings.Replace(title, "\t", "", -1)
break
}
//取内容 <div class="content-txt pt10"> 段子内容 <a id="prev" href="
re2 := regexp.MustCompile(`<div class="content-txt pt10">(?s:(.*?))<a id="prev" href="`)
if re2 == nil {
//fmt.Println("regexp.MustCompile err")
err = fmt.Errorf("%s", "regexp.MustCompile err2")
return
}
//取内容
tmpContent := re2.FindAllStringSubmatch(result, -1)
for _, data := range tmpContent {
content = data[1]
content = strings.Replace(content, "\t", "", -1)
content = strings.Replace(content, "\n", "", -1)
content = strings.Replace(content, "\r", "", -1)
content = strings.Replace(content, "<br />", "", -1)
break
}
return
}
//把内容写入到文件
func StoreJoyToFile(i int, fileTitle, fileContent []string) {
//新建文件
f, err := os.Create(strconv.Itoa(i) + ".txt")
if err != nil {
fmt.Println("os.Create err = ", err)
return
}
defer f.Close()
//写内容
n := len(fileTitle)
for i := 0; i < n; i++ {
//写标题
f.WriteString(fileTitle[i] + "\n")
//写内容
f.WriteString(fileContent[i] + "\n")
f.WriteString("\n=================================================================\n")
}
}
func SpiderPape(i int, page chan int) {
//明确爬取的url
//https://www.pengfu.com/xiaohua_1.html
url := "https://www.pengfu.com/xiaohua_" + strconv.Itoa(i) + ".html"
fmt.Printf("正在爬取第%d个网页:%s\n", i, url)
//开始爬取页面内容
result, err := HttpGet(url)
if err != nil {
fmt.Println("HttpGet err = ", err)
return
}
//fmt.Println("r = ", result)
//取,<h1 class="dp-b"><a href=" 一个段子url连接 "
//解释表达式
re := regexp.MustCompile(`<h1 class="dp-b"><a href="(?s:(.*?))"`)
if re == nil {
fmt.Println("regexp.MustCompile err")
return
}
//取关键信息
joyUrls := re.FindAllStringSubmatch(result, -1)
//fmt.Println("joyUrls = ", joyUrls)
fileTitle := make([]string, 0)
fileContent := make([]string, 0)
//取网址
//第一个返回下标,第二个返回内容
for _, data := range joyUrls {
//fmt.Println("url = ", data[1])
//开始爬取每一个笑话,每一个段子
title, content, err := SpiderOneJoy(data[1])
if err != nil {
fmt.Println("SpiderOneJoy err = ", err)
continue
}
//fmt.Printf("title = #%v#", title)
//fmt.Printf("content = #%v#", content)
fileTitle = append(fileTitle, title) //追加内容
fileContent = append(fileContent, content) //追加内容
}
//fmt.Println("fileTitle= ", fileTitle)
//fmt.Println("fileContent= ", fileContent)
//把内容写入到文件
StoreJoyToFile(i, fileTitle, fileContent)
page <- i //写内容,写num
}
func DoWork(start, end int) {
fmt.Printf("准备爬取第%d页到%d页的网址\n", start, end)
page := make(chan int)
for i := start; i <= end; i++ {
//定义一个函数,爬主页面
go SpiderPape(i, page)
}
for i := start; i <= end; i++ {
fmt.Printf("第%d个页面爬取完成\n", <-page)
}
}
func main() {
var start, end int
fmt.Printf("请输入起始页( >= 1) :")
fmt.Scan(&start)
fmt.Printf("请输入终止页( >= 起始页) :")
fmt.Scan(&end)
DoWork(start, end) //工作函数
}
爬虫总结
实际上爬虫一共就四个主要步骤:
- 明确目标 (要知道你准备在哪个范围或者网站去搜索)
- 爬 (将所有的网站的内容全部爬下来)
- 取 (去掉对我们没用处的数据)
- 处理数据(按照我们想要的方式存储和使用)