最近这一段时间都在学习Golang,想写点东西练练手。一开始是想用golang写爬虫什么的但是感觉太简单了,不如写一个网站吧!想了想我对P站很熟悉,之前有写过nginx反代P站视频的文章。不如结合起来自己写一个静态的P站吧,于是就做了一个demo出来。整个Golang的代码也不多,很适合新手练练手。
首先分析整个网站的构思,内容也就是图片视频标题连接从哪里来?这个当然是从Pornhub上直接抓取。其次既然是静态网站,那么除了视频页面的获取,其他的基本没有和后端的交互,页面也是后端渲染好直接给前端。而视频的播放就直接交给nginx来处理就行了。大致就这么简单,其他的细节后面详述。
我们第一步就是要先去P站上面抓取视频素材,因为是静态网站我们不用弄得过于复杂,可以每次只抓取一页的内容进行显示。众所众知,P站是一个学习网站同时也是文化包容性很强的网站!比如我们在P站上搜索美食为例,可以看到众多网友上传的美食视频。
假如说我们就抓取这上面的视频内容,打开控制台检查页面元素。
每个视频都是放在一个li标签里面的,包含了视频的一些基本信息。这就是我们需要抓取的内容。而原视频的链接是在该视频页面里才能获取到,所以需要再发起一次get请求。这里直接贴部分代码。
func getRandomPhid () [] string {
rand. Seed ( time. Now () . Unix ())
randNum := rand. Intn ( 1000 ) //播种生成随机整数
url := "https://cn.pornhub.com/video?page=" + strconv. Itoa ( randNum )
resp, err := http. Get ( url )
htmlText, err := ioutil. ReadAll ( resp. Body )
root, err := htmlquery. Parse ( strings. NewReader ( string ( htmlText )))
ulTag := htmlquery. FindOne ( root, "//*[@id=\"videoCategory\"]" )
aHrefList := htmlquery. Find ( ulTag, "//a[@href]" )
for _, n := range aHrefList {
aHref := htmlquery. SelectAttr ( n, "href" )
if strings. Contains ( aHref, "viewkey" ) {
if tmp == aHref { //提取视频链接时会有连续重复的情况
ph := strings. Split ( aHref, "=" )[ 1 ]
phList = append ( phList, ph )
func getRandomPhid() []string {
rand.Seed(time.Now().Unix())
randNum := rand.Intn(1000) //播种生成随机整数
url := "https://cn.pornhub.com/video?page=" + strconv.Itoa(randNum)
fmt.Println(url)
resp, err := http.Get(url)
checkError(err)
htmlText, err := ioutil.ReadAll(resp.Body)
checkError(err)
root, err := htmlquery.Parse(strings.NewReader(string(htmlText)))
checkError(err)
ulTag := htmlquery.FindOne(root, "//*[@id=\"videoCategory\"]")
aHrefList := htmlquery.Find(ulTag, "//a[@href]")
phList := []string{}
tmp := ""
for _, n := range aHrefList {
aHref := htmlquery.SelectAttr(n, "href")
if strings.Contains(aHref, "viewkey") {
if tmp == aHref { //提取视频链接时会有连续重复的情况
continue
}
ph := strings.Split(aHref, "=")[1]
phList = append(phList, ph)
tmp = aHref
}
}
return phList
}
func getRandomPhid() []string {
rand.Seed(time.Now().Unix())
randNum := rand.Intn(1000) //播种生成随机整数
url := "https://cn.pornhub.com/video?page=" + strconv.Itoa(randNum)
fmt.Println(url)
resp, err := http.Get(url)
checkError(err)
htmlText, err := ioutil.ReadAll(resp.Body)
checkError(err)
root, err := htmlquery.Parse(strings.NewReader(string(htmlText)))
checkError(err)
ulTag := htmlquery.FindOne(root, "//*[@id=\"videoCategory\"]")
aHrefList := htmlquery.Find(ulTag, "//a[@href]")
phList := []string{}
tmp := ""
for _, n := range aHrefList {
aHref := htmlquery.SelectAttr(n, "href")
if strings.Contains(aHref, "viewkey") {
if tmp == aHref { //提取视频链接时会有连续重复的情况
continue
}
ph := strings.Split(aHref, "=")[1]
phList = append(phList, ph)
tmp = aHref
}
}
return phList
}
这里我定义了一个getRandomPhid的函数,就是随机抓取P站上一页的视频内容。播种后生成0-1000里面的一个任意的整数。由于不需要对请求做额外的修改就直接使用了http.Get方法。这里使用了一个第三方库htmlquery来解析html元素,就类似于python里面的BeautifulSoup这种库。
因为所有的li标签的父标签是ul,通过xpath来定位元素。然后我定义了一个string类型的切片phList存放提取出来的视频id。因为会出现重复id,所以还需要去重,但是golang里面去重没有python方便可以直接用set,还需要自己实现,不过这里比较特殊,两个重复的id是连续的,所以直接用一个tmp变量来和上一个对比即可去重。
func reqPhid ( id string ) ( videoInfo map [ string ] string , err error ) {
if err := recover () ; err != nil {
url := "https://cn.pornhub.com/view_video.php?viewkey=" + id
req, err := http. NewRequest ( "GET" , url, nil )
req. Header . Set ( "user-agent" , "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" )
resp, err := client. Do ( req )
htmlText, err := ioutil. ReadAll ( resp. Body )
root, err := htmlquery. Parse ( strings. NewReader ( string ( htmlText )))
js := htmlquery. FindOne ( root, "//*[@id=\"mobileContainer\"]/script[1]/text()" )
jsString := htmlquery. InnerText ( js )
videoInfo = make ( map [ string ] string )
phQuality := gjson. Get ( jsString, "quality_720p" ) . String ()
err = errors. New ( "获取视频链接失败!" )
re := regexp. MustCompile ( `(?m)https:\/\/([a-z0-9]{2,3})\.phncdn\.com` )
subDomain := re. FindStringSubmatch ( phQuality )[ 1 ]
time. Sleep ( 3 * time. Second )
err = errors. New ( "未匹配dm1接口!" )
phQuality = re. ReplaceAllString ( phQuality, domain+subDomain )
image_url := gjson. Get ( jsString, "image_url" ) . String ()
subDomain = re. FindStringSubmatch ( image_url )[ 1 ]
image_url = re. ReplaceAllString ( image_url, domain+subDomain )
video_title := gjson. Get ( jsString, "video_title" ) . String ()
videoInfo [ "videoUrl" ] = phQuality
videoInfo [ "imgUrl" ] = image_url
videoInfo [ "title" ] = video_title
func reqPhid(id string) (videoInfo map[string]string, err error) {
defer func() {
if err := recover(); err != nil {
log.Println(err)
}
}()
client := &http.Client{}
url := "https://cn.pornhub.com/view_video.php?viewkey=" + id
req, err := http.NewRequest("GET", url, nil)
checkError(err)
req.Header.Set("user-agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1")
resp, err := client.Do(req)
checkError(err)
htmlText, err := ioutil.ReadAll(resp.Body)
checkError(err)
root, err := htmlquery.Parse(strings.NewReader(string(htmlText)))
checkError(err)
js := htmlquery.FindOne(root, "//*[@id=\"mobileContainer\"]/script[1]/text()")
jsString := htmlquery.InnerText(js)
videoInfo = make(map[string]string)
phQuality := gjson.Get(jsString, "quality_720p").String()
if phQuality == "" {
err = errors.New("获取视频链接失败!")
return
}
re := regexp.MustCompile(`(?m)https:\/\/([a-z0-9]{2,3})\.phncdn\.com`)
subDomain := re.FindStringSubmatch(phQuality)[1]
if subDomain != "dm1" {
time.Sleep(3 * time.Second)
err = errors.New("未匹配dm1接口!")
return
}
phQuality = re.ReplaceAllString(phQuality, domain+subDomain)
image_url := gjson.Get(jsString, "image_url").String()
subDomain = re.FindStringSubmatch(image_url)[1]
image_url = re.ReplaceAllString(image_url, domain+subDomain)
video_title := gjson.Get(jsString, "video_title").String()
videoInfo["videoUrl"] = phQuality
videoInfo["imgUrl"] = image_url
videoInfo["title"] = video_title
fmt.Println(videoInfo)
return
}
func reqPhid(id string) (videoInfo map[string]string, err error) {
defer func() {
if err := recover(); err != nil {
log.Println(err)
}
}()
client := &http.Client{}
url := "https://cn.pornhub.com/view_video.php?viewkey=" + id
req, err := http.NewRequest("GET", url, nil)
checkError(err)
req.Header.Set("user-agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1")
resp, err := client.Do(req)
checkError(err)
htmlText, err := ioutil.ReadAll(resp.Body)
checkError(err)
root, err := htmlquery.Parse(strings.NewReader(string(htmlText)))
checkError(err)
js := htmlquery.FindOne(root, "//*[@id=\"mobileContainer\"]/script[1]/text()")
jsString := htmlquery.InnerText(js)
videoInfo = make(map[string]string)
phQuality := gjson.Get(jsString, "quality_720p").String()
if phQuality == "" {
err = errors.New("获取视频链接失败!")
return
}
re := regexp.MustCompile(`(?m)https:\/\/([a-z0-9]{2,3})\.phncdn\.com`)
subDomain := re.FindStringSubmatch(phQuality)[1]
if subDomain != "dm1" {
time.Sleep(3 * time.Second)
err = errors.New("未匹配dm1接口!")
return
}
phQuality = re.ReplaceAllString(phQuality, domain+subDomain)
image_url := gjson.Get(jsString, "image_url").String()
subDomain = re.FindStringSubmatch(image_url)[1]
image_url = re.ReplaceAllString(image_url, domain+subDomain)
video_title := gjson.Get(jsString, "video_title").String()
videoInfo["videoUrl"] = phQuality
videoInfo["imgUrl"] = image_url
videoInfo["title"] = video_title
fmt.Println(videoInfo)
return
}
在上一个函数中我们获取到了所有的视频id,要获取真正的视频链接就需要再请求一次视频页面。在该函数中我一开始就defer了一个匿名函数来捕获异常。接下来我们没有用http.Get的方法,而是用&http.Client{}添加一些我们自定义的header,也就是修改了User-Agent伪装成手机的浏览器头,这样的服务端返回的也是h5的页面,原因是这里用到了一个小trick 。
看过我之前文章的读者都知道,请求Pc端的P站,拿到的视频信息都是被加密混淆过的,而手机端的都直接是原视频的信息。这个也是我偶然发现,不然这里又要费力不讨好了,所以思路还是非常重要的,有时能起到四两拨千斤的效果。不了解的朋友可以看下我之前的文章。
大家可以用Chrome试试切换浏览器UA,真的很方便!
还是先用xpath定位到这段JS的位置,这个flashvars的变量很明显是一个json格式的数据。U1S1,golang原生处理json的方法真的很繁琐,还要写个struct来接受数据,数据小的json还好,一些复杂的json特别是不知道数据类型的类型的时候简直裂开。这里我推荐一个第三方模块gjson来解析非常方便,和python里面一样丝滑。
另外视频的清晰度有240,480,720,1080这几种,为了尽可能简化,我只取了720的一种清晰度的链接。通过正则匹配转换为自己的域名反代出来的视频链接。最后返回一个map[string]string类型的数据。
re := regexp. MustCompile ( `(?m)https:\/\/([a-z0-9]{2,3})\.phncdn\.com` )
subDomain := re. FindStringSubmatch ( phQuality )[ 1 ]
time. Sleep ( 3 * time. Second )
err = errors. New ( "未匹配dm1接口!" )
re := regexp.MustCompile(`(?m)https:\/\/([a-z0-9]{2,3})\.phncdn\.com`)
subDomain := re.FindStringSubmatch(phQuality)[1]
if subDomain != "dm1" {
time.Sleep(3 * time.Second)
err = errors.New("未匹配dm1接口!")
return
}
re := regexp.MustCompile(`(?m)https:\/\/([a-z0-9]{2,3})\.phncdn\.com`)
subDomain := re.FindStringSubmatch(phQuality)[1]
if subDomain != "dm1" {
time.Sleep(3 * time.Second)
err = errors.New("未匹配dm1接口!")
return
}
至于这里为什么只能用dm1接口的链接我们后面再讨论。而且为了防止请求频率过快导致被ban的情况,每次抓取后暂停3s尽可能减少异常。
allVideos = make ([] map [ string ] string , 0 )
phList := getRandomPhid ()
for _, id := range phList {
video, err := reqPhid ( id )
allVideos = append ( allVideos, video )
log. Printf ( "第%v次刷新页面" , count )
time. Sleep ( 15 * time. Minute ) //每隔多长时间时间刷新一次页面
func timeToRefresh() {
count := 0 //记录页面刷新的次数
for true {
allVideos = make([]map[string]string, 0)
phList := getRandomPhid()
for _, id := range phList {
video, err := reqPhid(id)
if err != nil {
log.Println(err)
continue
}
allVideos = append(allVideos, video)
}
count++
log.Printf("第%v次刷新页面", count)
time.Sleep(15 * time.Minute) //每隔多长时间时间刷新一次页面
}
}
func timeToRefresh() {
count := 0 //记录页面刷新的次数
for true {
allVideos = make([]map[string]string, 0)
phList := getRandomPhid()
for _, id := range phList {
video, err := reqPhid(id)
if err != nil {
log.Println(err)
continue
}
allVideos = append(allVideos, video)
}
count++
log.Printf("第%v次刷新页面", count)
time.Sleep(15 * time.Minute) //每隔多长时间时间刷新一次页面
}
}
因为我们是写的一个静态页面,所以不存在前后端交互,都是后端渲染好,所以后端就要定时抓取来刷新我们网站的内容。这里allVideos是一个map来存放每一次抓取的视频信息,但是注意这里在for循环中每次都用make来初始化了这个变量,相当于是刷新了每次的视频数据。同时allVideos还是一个全局变量让myHandler直接使用。
到这里拿到所有的视频相关信息基本抓取就结束了。剩下的就是webserver的来做的事情了。其实在golang里面也特别简单。
func myHandler ( w http. ResponseWriter , r *http. Request ) {
t, err := template. ParseFiles ( "index.html" )
err = t. Execute ( w, allVideos )
http. HandleFunc ( "/" , myHandler )
err := http. ListenAndServe ( ":8080" , nil )
func myHandler(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("index.html")
checkError(err)
err = t.Execute(w, allVideos)
checkError(err)
}
func main() {
go timeToRefresh()
http.HandleFunc("/", myHandler)
err := http.ListenAndServe(":8080", nil)
checkError(err)
}
func myHandler(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("index.html")
checkError(err)
err = t.Execute(w, allVideos)
checkError(err)
}
func main() {
go timeToRefresh()
http.HandleFunc("/", myHandler)
err := http.ListenAndServe(":8080", nil)
checkError(err)
}
这里我就把两个函数放到一起了,myHandler相当于是对请求做的一个响应。首先是解析我们写好的前端模板再渲染生成好我们传入的数据就发送给用户了。
timeToRefresh函数前面说了是一个定时抓取的函数,但是我们不能让抓取页面阻塞了我们的主线程,所以直接在他前面加一个go关键字即可,是不是非常的方便呢,这就体现了go语言的魅力,在其他语言里面我们还再写个thread什么的。所以golang写起来真的无比丝滑。然后就是绑定路径和监听端口什么的了,这些都很简单了。
func checkError ( err error ) {
func checkError(err error) {
if err != nil {
log.Println(err)
}
}
func checkError(err error) {
if err != nil {
log.Println(err)
}
}
还有大家都知道golang里面的错误处理会写一堆 if err !=nil{ … } 这种,所以我就再封装一层,上面代码中看到我统一用checkError来处理err。
Golang的部分就基本说完了,下面说一下Nginx的部分,既然要播放视频,光抓取到了视频链接还不行,因为P站在国内是被墙了的,没办法直接播放。所有可以采用Nginx反代视频流开启buffer的方式实现,具体可以参考我之前的一篇文章。
在这篇文章中我通过多个多个子域名的形式来反代,其实没有必要之前读者留言说过,可以用下面这种写法。
之前的代码中就是提到过没有匹配到dm1接口的时候就会panic,因为这里我还没有研究透彻就是我反代dm1域名是没有问题的,但是反代其他的会403拒绝响应。
而dm1的则是正常的。所以这里我只用了匹配到dm1的视频链接。
我猜测应该是做了一些检测,但是我弄清楚具体的原因,不知道是不是要带上某些特定的header头才行,希望有会的朋友可以留言一起探讨一下。
最后是前端模板了,这个是让勾大佬帮我做的,非常感谢勾大佬的帮忙。其中let json = {{ .}} 就是golang模板的语法,传入我们的数据。
< meta name= "viewport" content= "width=device-width, initial-scale=1.0" >
< title > Golang + Nginx 实现静态 Pornhub < /title >
appendCard ( newCard ( v. videoUrl , v. imgUrl , v. title ))
function newCard ( videoUrl, imgUrl, title ) {
let card = document. createElement ( 'div' )
card. classList . add ( 'card' )
let face1 = document. createElement ( 'div' )
face1. classList . add ( 'face' )
face1. classList . add ( 'face1' )
let p = document. createElement ( 'p' )
p. setAttribute ( "videoUrl" , videoUrl )
let face2 = document. createElement ( 'div' )
face2. classList . add ( 'face' )
face2. classList . add ( 'face2' )
let img = document. createElement ( 'img' )
img. setAttribute ( 'src' , imgUrl )
img. setAttribute ( 'alt' , "" )
p. onclick = function ( e ) {
window. open ( e. target . getAttribute ( "videoUrl" ))
function appendCard ( card ) {
let main = document. getElementsByClassName ( 'main' )[ 0 ]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Golang + Nginx 实现静态 Pornhub </title>
<style>
//样式的代码过长,就不展示了
</style>
</head>
<body>
<h3>每隔15分钟自动刷新视频页面</h3>
<div class="main">
</div>
<script>
//后端返回的数据
let json = {{.}}
//处理数据
json.map(v => {
appendCard(newCard(v.videoUrl, v.imgUrl, v.title))
})
//创建card
function newCard(videoUrl, imgUrl, title) {
let card = document.createElement('div')
card.classList.add('card')
let face1 = document.createElement('div')
face1.classList.add('face')
face1.classList.add('face1')
let p = document.createElement('p')
p.innerText = title
p.setAttribute("videoUrl", videoUrl)
face1.appendChild(p)
let face2 = document.createElement('div')
face2.classList.add('face')
face2.classList.add('face2')
let img = document.createElement('img')
img.setAttribute('src', imgUrl)
img.setAttribute('alt', "")
face2.appendChild(img)
card.appendChild(face1)
card.appendChild(face2)
p.onclick = function (e) {
window.open(e.target.getAttribute("videoUrl"))
}
return card
}
//将card添加进页面
function appendCard(card) {
let main = document.getElementsByClassName('main')[0]
main.appendChild(card)
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Golang + Nginx 实现静态 Pornhub </title>
<style>
//样式的代码过长,就不展示了
</style>
</head>
<body>
<h3>每隔15分钟自动刷新视频页面</h3>
<div class="main">
</div>
<script>
//后端返回的数据
let json = {{.}}
//处理数据
json.map(v => {
appendCard(newCard(v.videoUrl, v.imgUrl, v.title))
})
//创建card
function newCard(videoUrl, imgUrl, title) {
let card = document.createElement('div')
card.classList.add('card')
let face1 = document.createElement('div')
face1.classList.add('face')
face1.classList.add('face1')
let p = document.createElement('p')
p.innerText = title
p.setAttribute("videoUrl", videoUrl)
face1.appendChild(p)
let face2 = document.createElement('div')
face2.classList.add('face')
face2.classList.add('face2')
let img = document.createElement('img')
img.setAttribute('src', imgUrl)
img.setAttribute('alt', "")
face2.appendChild(img)
card.appendChild(face1)
card.appendChild(face2)
p.onclick = function (e) {
window.open(e.target.getAttribute("videoUrl"))
}
return card
}
//将card添加进页面
function appendCard(card) {
let main = document.getElementsByClassName('main')[0]
main.appendChild(card)
}
</script>
</body>
</html>
这样我们的静态P站就做好了,最后就是展示一下我们用Golang+Nginx做出来网站了。我这里用到某云的香港轻量服务器来跑的,有30m的带宽,反代视频基本上是没有压力的。
由于尺度过大这里就全部打码了,再加上网站背景换成橙黄色是不是就有P站那味了哈哈哈。同时点击视频就能直接播放了,是不是很香呢!看来ghs也没那么难嘛!当然这里单纯的只是技术分享罢了哈哈!
这边博客纯粹是我学习Golang一段时间后练手做的一个小demo,感兴趣的小伙伴可以自己动手试试,毕竟学以致用嘛!我把代码和前端模板以及nginx的配置文件放在下面,就是把golang代码和Nginx配置其中的域名换成自己的域名即可! 大家简单改一改就能跑起来了。
golang+nginx-制作一个静态pornhub网站.zip
有什么问题小伙伴可以在下面留言一起讨论!
Post Views: 4,710
赞赏 微信赞赏 支付宝赞赏
25条评论