Golang + Nginx 动手写一个静态Pornhub网站

微信扫一扫,分享到朋友圈

Golang + Nginx 动手写一个静态Pornhub网站

最近这一段时间都在学习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)
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) {
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站,拿到的视频信息都是被加密混淆过的,而手机端的都直接是原视频的信息。这个也是我偶然发现,不然这里又要费力不讨好了,所以思路还是非常重要的,有时能起到四两拨千斤的效果。不了解的朋友可以看下我之前的文章。

看完Pornhub的视频接口JS混淆后,我顺手写了个下载插件

大家可以用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]
if subDomain != "dm1" {
time.Sleep(3 * time.Second)
err = errors.New("未匹配dm1接口!")
return
}

至于这里为什么只能用dm1接口的链接我们后面再讨论。而且为了防止请求频率过快导致被ban的情况,每次抓取后暂停3s尽可能减少异常。

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")
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) {
if err != nil {
log.Println(err)
}
}

还有大家都知道golang里面的错误处理会写一堆 if err !=nil{ … } 这种,所以我就再封装一层,上面代码中看到我统一用checkError来处理err。

Golang的部分就基本说完了,下面说一下Nginx的部分,既然要播放视频,光抓取到了视频链接还不行,因为P站在国内是被墙了的,没办法直接播放。所有可以采用Nginx反代视频流开启buffer的方式实现,具体可以参考我之前的一篇文章。

白嫖?妙用Nginx的proxy_buffer实现Pornhub视频反代

在这篇文章中我通过多个多个子域名的形式来反代,其实没有必要之前读者留言说过,可以用下面这种写法。


之前的代码中就是提到过没有匹配到dm1接口的时候就会panic,因为这里我还没有研究透彻就是我反代dm1域名是没有问题的,但是反代其他的会403拒绝响应。


而dm1的则是正常的。


Post Views: 18

赞赏

微信赞赏 支付宝赞赏

微信扫一扫,分享到朋友圈

Golang + Nginx 动手写一个静态Pornhub网站

Facebook:在年终购物季投放更长时间的广告对品牌更有利

上一篇

净利润同比近5年最低,新城控股进入“二代”王晓松手中,成色几何?

下一篇

你也可能喜欢

Golang + Nginx 动手写一个静态Pornhub网站

长按储存图像,分享给朋友