思路想法
AJAX
先简单扫盲一下什么是AJAX,个人建议如果以下提到的几个名词你都没听说过或者只简单用过,那么你最好还是回头把每一项都补一补,虽然这对写爬虫帮助不大,但是对你深入理解计算机这门学科帮助很大。须知不管是语言,还是框架都有其存在的目的,搞清楚了这些,学起东西来就相对容易了。
AJAX (Asynchronous JavaScript and XML),直译的话就是异步的JavaScript和XML。实际上,这个名字有一定的误导性:据我所知,AJAX至少还支持json文件,可能是这个技术发明的时候只有JavaScript和XML存在才这样命名的。这里的异步指的是异步加载或者异步数据交换,指的是利用XMLHttpRequst或其他fetch API在网页初步加载结束后,再次发送请求并从服务器上获取并解析数据,然后把这部分数据添加到到已有的页面上,在这个过城中,访问的URL始终没变。
AJAX本质上是一种框架,通过JavaScript可以实现部分更新网页的效果。这样可以节省互联网中的传输带宽。JavaScript是一种浏览器脚本语言,它可以通过不访问服务器达到修改客户端的目的。比如说,有的网站有很多选项,当你选了其中一个选项,会出现更多的子选项,这其实就是Javascript做的,直到你点了提交,浏览器才会和服务器交互。AJAX框架里用到的只是JavaScript众多功能的一小部分。
今日头条
Hmm...其实我也不喜欢今日头条,推送的内容越来越像UC头条了。如果你在头条页面上搜索关键词古镇,你会得到若干文章,如果你一直下拉滑动条到底部,你会发现浏览器会自动加载更多的文章,这就是AJAX的一种表现。
然而,万变不离其宗,不管是什么技术,框架,只要HTTP协议仍在使用,都跑不过GET/POST和其响应,我们要做的就是找到这条关键的GET请求。
有没有用AJAX
判断一个网站是不是用了AJAX可以从以下几点看出端倪:
- 最实锤的,就是用浏览器审查元素的时候,在‘Network’里过滤XHR标签。XHR应该就是XMLHttpRequest这是AJXAX的一个特征。
- 像今日头条这样的网站会有明显的异步加载现象(就是往下拖的时候会出现新的内容)。
- 查看网页源代码,注意不是审查元素。审查是看不出来的,因为有可能异步加载已经完成了,你也许可以从审查元素里找到你要的HTML结构,但它们不是requests返回的而是javascript加载的。
- 如果一个网页源码里充斥这各种JavaScript,而html代码很少,这种也很有可能是AJAX。
综上,其实还是很容易看出来的。
分析搜索页面
toutiao-索引页分析.jpg如图中1位置过滤XHR标签,会在位置2看到其报文。第一次访问时只有一条,随着进度条下拉,会不断出现新的请求。分析这四个报文,你可以在它的request的Header中找到参数列表,我们只需要修改其参数值就行了,其他参数也可以改,其中keyword就是你搜索的关键词。
Requests URLs(只有offset变化了):
https://www.toutiao.com/search_content/?
offset=0&format=json&keyword=%E5%A1%9E%E5%B0%94%E8%BE%BE%E4%BC%A0%E8%AF%B4&autoload=true&count=20&cur_tab=1&from=search_tab
https://www.toutiao.com/search_content/?
offset=20&format=json&keyword=%E5%A1%9E%E5%B0%94%E8%BE%BE%E4%BC%A0%E8%AF%B4&autoload=true&count=20&cur_tab=1&from=search_tab
https://www.toutiao.com/search_content/?
offset=40&format=json&keyword=%E5%A1%9E%E5%B0%94%E8%BE%BE%E4%BC%A0%E8%AF%B4&autoload=true&count=20&cur_tab=1&from=search_tab
......
这样我们就有了要访问的链接,随着offset=0, 20, 40, 60...增加,每条请求都会返回如上图中位置3的一个json结构,里面包含了不超过20条记录。如果json结构中data项目为空,说明已经没有结果了,标识爬虫结束。有时候搜索结果会很多很多,也可以设一个上限,比如说最多爬去前50个搜索结果。
toutiao-两种类型.jpg仔细看的话,搜索结果会有两种,一种是文字+图片的结构,包含图片和文字;另一种就是相册形式的结构,会包含若干张照片。这两种结果可以根据json结构里has_gallery
和has_image
字段来区分。
每条结果对应的URL我选用的是article_url
字段的值,但其实这里有个问题,再后来的爬取过程中,我发现这个字段的值并不全是头条的文章,有的是文章的源地址,估计是头条从网上爬来在规整加到自己的结果里的,一般出现在前几个。这种情况后面写的对应的爬取代码就不适用了,谁知道文章源地址用了什么样的结构。不过考虑到这种情况不多,我就简单过滤了一下,反正少爬几个页面问题也不大。
如果有强迫症,我考虑的一个方法是采用sourcs_url
字段的内容,但是这个地址会被重定向,所以get请求要调整一下:
https://www.toutiao.com/group/6544161999004107271/
Redirect to:
https://www.toutiao.com/a6544161999004107271/
带图集的页面
toutiao-带图集.jpg如图就是这类页面中的图集,我们要把这12张图片地址找出来。因为已经确定用了AJAX,这里要查看其网页源代码,之后就能找到:
toutiao-带图集-js.jpg内容有时会略显不同,但是是很容易找的。之后用正则表达式把你要的图片地址过滤出来就行。这里有个技巧,就是这部分的源码正好是json格式,你就可以把整个{}过滤出来之后load成一个字典,便于访问。
不带图集的页面
不带图集的页面就是文字+图片的模式,不过头条的这种页面也是动态加载的,我一开始以为这是静态页面,看了源代码才知道。
toutiao-不带图集-js.jpg找图片地址也很容易,仍然用正则表达式把它们过滤出来。
代码
代码结构
- 自建URL,调整offset和keyword取回索引页面,分析json.get('data')的结构,区分两类页面,将提取的URL放到各自对应的list中。
- 两个解析函数,分别用来解析两种页面,并把提取出的图片地址写入文件。
几个小问题:
- 我发现即使是同样的内容,正则会有时有结果有时没结果,不知道为什么?正则库不靠谱?这里如果正则没结果会被舍弃掉,并打印一条错误信息,程序继续。
- 关于处理带图集页面的函数中使用的正则式
'gallery: JSON\.parse\("(.*?)max_img'
,这里的.
之前也要转义符。虽然.
代表了任意字符,这里加不加结果不会变。但是如果内容里还有如JSON?parse
这样的表达式,不带转义符是过滤不出来的。 - 这个代码非常适合用多线程,一条线程爬,一条处理带图集的页面,一条处理不带图集的页面,处理的时候对两个list上锁。(我代码里没实现多线程)
jinritoutiao.py
configure.py请参考拙作:爬取糗事百科的内容和图片并展示。
import requests
import json
import time
import re
from random import choice
import configure
url = "https://www.toutiao.com/search_content/?"
header = {'user-agent': choice(configure.FakeUserAgents)}
keyword = '塞尔达传说'
has_gallery_lists = []
no_gallery_lists = []
def SearchPageParser(offset = 0):
payload = {
'offset':offset,
'format':'json',
'keyword':keyword,
'autoload':'true',
'count':30,
'cur_tab':1,
'from':'search_tab'
}
count = 0
try:
response = requests.get(url, headers=header, params=payload)
content = None
print ("Parser " + response.url)
if response.status_code == requests.codes.ok:
content = response.text
data = json.loads(content)
if not data:
return
for article in data.get('data'):
if True == article.get('has_gallery') and True == article.get('has_image'):
has_gallery_lists.append(article.get('article_url'))
count += 1
if False == article.get('has_gallery') and True == article.get('has_image'):
no_gallery_lists.append(article.get('article_url'))
count += 1
return count
except Exception as e:
print (e)
return
def SaveImage(imageURL):
# 这里就不下载了,只是把单纯写入文件
print (imageURL)
with open('toutiao.txt', 'a') as file:
file.write(imageURL + '\n')
def HasGalleryParser():
if 0 == len(has_gallery_lists):
return
# 这里写的时候注意(, ), ", ., 都是要转义的。
pattern = re.compile('gallery: JSON\.parse\("(.*?)max_img', re.S)
while has_gallery_lists:
this = has_gallery_lists.pop()
try:
response = requests.get(this, headers=header)
content = None
if response.status_code == requests.codes.ok:
content = response.text
data = pattern.findall(content)
if data:
data = data[0][:-4].replace('\\','') + ']}'
img_urls = json.loads(data).get('sub_images')
for img_url in img_urls:
SaveImage(img_url.get('url'))
else:
print ("BadPageURL[GalleryParser, {0:s}]".format(this))
except Exception as e:
print (e)
return
time.sleep(0.25)
def NoGalleryParser():
if 0 == len(no_gallery_lists):
return
while no_gallery_lists:
this = no_gallery_lists.pop()
pattern = re.compile('<img src="(.*?)"', re.S)
try:
response = requests.get(this, headers=header)
content = None
if response.status_code == requests.codes.ok:
content = response.text
img_urls = pattern.findall(content)
if img_urls:
for img_url in img_urls:
SaveImage(img_url)
else:
print ("BadPageURL[NoGalleryParser, {0:s}]".format(this))
except Exception as e:
print (e)
return
time.sleep(0.25)
if __name__ == "__main__":
x, count = 0, 0
cnt_urls = SearchPageParser(x)
while count < 20 and cnt_urls:
cnt_urls = SearchPageParser(x+20)
count += cnt_urls
x += 20
time.sleep(0.55)
print ("Get {0:d} URL(s) in total.".format(count))
HasGalleryParser()
NoGalleryParser()
网友评论