写完爬取糗百热门后没几天,又开始写了爬取今日头条图片的工具。
灵感来源于Python 福利小爬虫,爬取今日头条街拍美女图,作者很详细的分析了今日头条一个搜索接口,并列出了步骤。
而我用Go写的,稍稍做了改动,加入了可以自定义爬取标签的功能,并在写本文前完成了以 “标签/文章名/图片名” 结构存储图片的功能。
分析网页依然使用goquery。
分析接口返回结构
{
"count": 30,
"action_label": "click_search",
"return_count": 0,
"has_more": 0,
"page_id": "/search/",
"cur_tab": 1,
"offset": 150,
"action_label_web": "click_search",
"show_tabs": 1,
"data": [
{
"play_effective_count": "6412",
"media_name": "开物志",
"repin_count": 49,
"ban_comment": 0,
"show_play_effective_count": 1,
"abstract": "",
"display_title": "",
"datetime": "2016-12-13 21:35",
"article_type": 0,
"more_mode": false,
"create_time": 1481636117,
"has_m3u8_video": 0,
"keywords": "",
"video_duration": 161,
"has_mp4_video": 0,
"favorite_count": 49,
"aggr_type": 0,
"article_sub_type": 0,
"bury_count": 2,
"title": "沃尔沃Tier 4 Final大型引擎的工作原理揭秘",
"has_video": true,
"share_url": "http://toutiao.com/group/6363577276176531969/?iid=0&app=news_article",
"id": 6363577276176532000,
"source": "开物志",
"comment_count": 4,
"article_url": "http://toutiao.com/group/6363577276176531969/",
"image_url": "http://p3.pstatp.com/list/12f0000909de79ceeabc",
"middle_mode": true,
"large_mode": false,
"item_source_url": "/group/6363577276176531969/",
"media_url": "http://toutiao.com/m6643043415/",
"display_time": 1481635793,
"publish_time": 1481635793,
"go_detail_count": 2290,
"image_list": [],
"item_seo_url": "/group/6363577276176531969/",
"video_duration_str": "02:41",
"source_url": "/group/6363577276176531969/",
"tag_id": 6363577276176532000,
"natant_level": 0,
"seo_url": "/group/6363577276176531969/",
"display_url": "http://toutiao.com/group/6363577276176531969/",
"url": "http://toutiao.com/group/6363577276176531969/",
"level": 0,
"digg_count": 4,
"behot_time": 1481635793,
"tag": "news_car",
"has_gallery": false,
"has_image": false,
"highlight": {
"source": [],
"abstract": [],
"title": []
},
"group_id": 6363577276176532000,
"middle_image": "http://p3.pstatp.com/list/12f0000909de79ceeabc"
},
],
"message": "success",
"action_label_pgc": "click_search"
}
嗯,特别多,其实只需要 data 里的内容就可以了。
所以
构造一个请求结果的struct。
type ApiData struct {
Has_more int `json:"has_more"`
Data []Data `json:"data"`
}
再看下data里,嗯,没用的又一大堆。
只需要文章链接就够了。
type Data struct {
Article_url string `json:"article_url"`
}
有了文章链接,那就好说了,啥都好商量。
分析文章结构
id=“J_content” 下是文章的主要内容,class=“article-title”是文章标题,class=“article-content”里是文章内容,只需要article-content里所有img元素就可以了。
type Img struct {
Src string `json:"src"`
}
由于需要一直更改查询接口的offset参数,所以直接把接口地址拿到外边做了全局变量。并且默认存在下一页。tag用来表示当前爬取的标签的名称。
var (
host string = "http://www.toutiao.com/search_content/?format=json&keyword=%s&count=30&offset=%d"
hasmore bool = true
tag string
)
正菜
0. 接收参数
首先,接收并遍历命令行中传入的标签。
func main() {
for _, tag = range os.Args[1:] {
hasmore = true
getByTag()
}
log.Println("全部抓取完毕")
}
每个循环开始时重置 hasmore 。
1. 循环请求接口
func getByTag() {
i, offset := 1, 0
for {
if hasmore {
log.Printf("标签: '%s',第 '%d' 页, OFFSET: '%d' \n", tag, i, offset)
tmpUrl := fmt.Sprintf(host, tag, offset)
getResFromApi(tmpUrl)
offset += 30
i++
time.Sleep(500 * time.Millisecond)
} else {
break
}
}
log.Printf("标签: '%s', 共 %v 页,爬取完毕\n", tag, i-1)
}
重置当前页,和当前offset。页数从第一页开始,主要是显示进度看起来更人性化一些。但是程序员的世界是从0开始。。。想改成0就改成0吧。
hasmore = true 表示存在下一页,使用fmt包的Sprintf方法格式化请求链接。然后对offset+30,对当前页i+1。再之后停顿了500毫秒。
这里其实有个问题,如果实际内容以每页30请求,可能恰好有150条,即每页数量的整数倍,但是这个时候接口返回的has_more依然等于1,即服务端认为还有下一页。。。但是其实没有了,所以会有一次空循环。
2. 处理请求结果
func getResFromApi(url string) {
resp, err := http.Get(url)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
var res ApiData
json.Unmarshal([]byte(string(body)), &res)
for _, item := range res.Data {
getImgByPage(item.Article_url)
}
if res.Has_more == 0 {
hasmore = false
}
}
没啥说的,拿到每一个请求接口的链接后打开,把结果数组中的data解析到ApiData中,于是就拿到了文章链接,然后遍历处理。
遍历完后要看下has_more的值,如果为0表示没有下一页了,修改全局变量hasmore的值,结束最外层的循环。
3. 处理文章
func getImgByPage(url string) {
//部分请求结果中包含其他网站的链接,会导致下面的query出现问题
if strings.Contains(url, "toutiao.com") {
doc, err := goquery.NewDocument(url)
if err != nil {
log.Fatal(err)
}
title := doc.Find("#article-main .article-title").Text()
title = strings.Replace(title, "/", "", -1)
os.MkdirAll(tag+"/"+title, 0777)
doc.Find("#J_content .article-content img").Each(func(i int, s *goquery.Selection) {
src, _ := s.Attr("src")
log.Println(title, src)
getImgAndSave(src, title)
})
}
}
最外层加了判断,是因为有一部分结果的链接是其他网站的。。。。
虽然这个判断很low,但是也够用了。
然后终于该用上goquery了,拿到标题,然后遍历文章内容中的img标签,就拿到了每一篇文章的每一张图片。
4. 保存图片
在上一步把图片地址和文章名称传递给了getImgAndSave。
func getImgAndSave(url string, dirname string) {
path := strings.Split(url, "/")
var name string
if len(path) > 1 {
name = path[len(path)-1]
}
resp, err := http.Get(url)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Fatal("请求失败", err)
return
}
contents, err := ioutil.ReadAll(resp.Body)
defer func() {
if x := recover(); x != nil {
return
}
}()
err = ioutil.WriteFile("./"+tag+"/"+dirname+"/"+name+".jpg", contents, 0644)
if err != nil {
log.Fatal("写入文件失败", err)
}
}
先分割图片链接,把最后一个”/“后的内容当成文件名。
后边get图片内容,但是有时候会出现对方服务器出错的情况,http状态码为500,所以加了判断请求是否成功的判断。
然后就是读取内容,保存到文件中了。
这里使用了WriteFile方式,查资料的时候还看到有闲Create文件,然后io.Copy写入的。
到这里就结束了。
RUN
go run main.go 美女 模特
等着看图吧。
github地址:toutiaoSpider,欢迎star。