网页 抓取 innertext 试题(一个demoP站视频标题连接从哪里来?(图))
优采云 发布时间: 2022-01-06 15:06网页 抓取 innertext 试题(一个demoP站视频标题连接从哪里来?(图))
最近在学习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定位。然后我定义了一个字符串类型的切片 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。要获得真正的视频链接,我们需要再次请求视频页面。在这个函数中,我从一开始就推迟了一个匿名函数来捕获异常。接下来我们没有使用http.Get方法,而是使用&http.Client{}添加了一些我们自定义的header,即修改User-Agent伪装成手机浏览器header,而这个server也返回h5页面,原因是这里用了一个小技巧。
看过我文章的读者都知道,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接口链接,我们后面再讨论。并且为了防止请求频率因为请求频率过快而被禁止,每次爬取后,暂停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 直接使用的全局变量。
在这里获取所有与视频相关的信息,基本上抓住它就结束了。剩下的就是网络服务器所做的。其实在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语言的魅力,我们会另外写一个线程或者其他语言的东西。所以golang写起来真的很流畅。然后是绑定路径和*敏*感*词*端口。这些都非常简单。
func checkError(err error) {
if err != nil {
log.Println(err)
}
}
而且大家都知道golang中的错误处理会写一堆if err !=nil{… },所以我再封装一层。在上面的代码中,我看到我使用 checkError 统一处理 err。
Golang的部分基本完成。再说说Nginx的部分。既然要播放视频,就不能随便抢视频链接,因为P站在国内被屏蔽了,不能直接播放。一切都可以通过使用Nginx反生成视频流打开缓冲区来实现。具体可以参考我之前的文章文章。
白嫖?神奇使用Nginx proxy_buffer实现Pornhub视频反向生成
在这个文章中,我使用了多个子域来逆向生成。事实上,读者无需在此之前发表评论。您可以使用以下编写方法。
前面的代码提到了不匹配dm1接口的时候会panic,因为我这里没有研究透彻。我把dm1域名倒过来是可以的,但是其他的会以403拒绝响应。
dm1 是正常的。所以这里我只使用了匹配dm1的视频链接。
我猜应该是做了一些测试,但是我想通了具体的原因,不知道有没有必要带一些具体的header。希望朋友们留言一起讨论。
最后是前端模板。这是勾刀为我做的。非常感谢勾刀的帮助。其中 let json = {{.}} 是 golang 模板的语法,传入我们的数据。
Golang + Nginx 实现静态 Pornhub
//样式的代码过长,就不展示了
每隔15分钟自动刷新视频页面
//后端返回的数据
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)
}
这样我们的静态P站就做好了,最后就是显示我们用Golang+Nginx网站做了。我在某云中使用了一台香港轻量级服务器在这里运行。它的带宽为30m。反代视频基本无压力。
因为比例太大,这里全部编码完成,网站的背景改为橙色。有没有P站的味道?哈哈哈。同时点击视频直接播放,是不是很香!好像ghs没那么难!当然,这里只是技术分享,哈哈!
本博客纯粹是我学习Golang一段时间后做的一个小demo。有兴趣的朋友可以自行尝试。毕竟,您可以应用所学!我把代码、前端模板和nginx配置文件放在下面,把golang代码和Nginx配置中的域名换成自己的域名即可!每个人都可以通过简单的改变开始跑步。
golang+nginx-做一个静态的pornhub网站.zip
如果有什么问题,小伙伴们可以在下方留言,共同探讨!