爬取今日头条街拍图片

项目地址:https://github.com/snjl/python.spider.jiepai.git

爬取网址

https://www.toutiao.com/search/?keyword=%E8%A1%97%E6%8B%8D

前期研究

访问网址后,会发现列表页有多种形式,第一种是视频,第二种是广告,第三种是所有图片在一个网页里面,第四种是图片需要跳转,我们做的是第四种。

通过f12开发者工具可以看到比较详细的信息。
点preserve log,可以刷新时保留记录,点XHR,可以看到传输过来的ajax数据。

列表页分析

列表是通过ajax传输过来的,参数有多个,在python爬取的时候需要传输:

1
2
3
4
5
6
7
8
9
10
11
data = {
'aid': 24,
'offset': offset,
'format': 'json',
'keyword': keyword,
'autoload': 'true',
'count': 20,
'cur_tab': 3,
'from': 'gallery',
'pd': 'synthesis',
}

其中keyword是可以改变的,offset是偏移值,一般是为20的倍数,因为每次json传输20个数据。

列表页需要获取的是每个详情页的title和url。

详情页分析

分析详情页,可以发现每次跳转网页实际上不是ajax交互,而是在初始的网页中有数据,如下所示:

1
2
3
    gallery: JSON.parse("{\"count\":9,\"sub_images\":[{\"url\":\"http:\\/\\/p1.pstatp.com\\/origin\\/pgc-image\\/e4de890cc2084bc5b3557ee5b6ea0ed9\",\"width\":800,\"url_list\":[{\"url\":\"http:\\/\\/p1.pstatp.com\\/origin\\/pgc-image\\/e4de890cc2084bc5b3557ee5b6ea0ed9\"},

···

所以实际上已经把图的地址写在doc里,拿到网页的url,然后通过正则获取就可以得到地址。

代码功能

三部分

列表页

列表页需要两个函数,第一个是获取列表页的的response.text(需要进行错误处理),第二个是对列表页进行解析,获取到需要抓取的详情页的一些信息。

详情页

详情页里,需要通过正则获取到所有需要下载的链接,所以也是两个函数,一个获取详情页的response.text,一个获取详情页中的下载信息。

数据库、图片下载等

数据库使用mongodb,这里的代码可以插入在详情页的下载信息或者main函数中每次处理完一个详情页信息后进行存储;

图片下载同样处理,也可以放在获取到详情页的下载链接后的for循环遍历。

具体代码

列表页

获取列表页response.text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def get_page_index(offset, keyword):
data = {
'aid': 24,
'offset': offset,
'format': 'json',
'keyword': keyword,
'autoload': 'true',
'count': 20,
'cur_tab': 3,
'from': 'gallery',
'pd': 'synthesis',
}
url = 'https://www.toutiao.com/api/search/content/?' + urlencode(data)
print(url)
try:
response = requests.get(url=url, headers=headers)
if response.status_code == 200:
response.encoding = 'utf8' # 原编码不是utf8,会有一定乱码
return response.text
return None
except RequestException as e:
print(e)
print("请求json出错")
return None

传入keyword,offset获取列表页text。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def parse_page_index(html):
try:
data = json.loads(html)
# 判断data不为空,且'data'在keys中,并且data['data']不为空
if data and 'data' in data.keys() and data['data']:
items = data['data']
for item in items:
# 判断abstract为空,且app_info存在,且item['app_info']['db_name']为SITE
if item and 'abstract' in item.keys() and \
item['abstract'] == '' and 'app_info' in item.keys() \
and item['app_info']['db_name'] == 'SITE':
yield {
'title': item['title'],
'url': item['article_url']
}
except JSONDecodeError as e:
print(e)

由于拿到的是ajjax的json数据,所以可以通过json.loads()方法解析到数据,然后判断data是否为空,data里面是否有‘data’字段,如果有的话判断里面的每一个item是否有’abstract’字段和’app_info’字段,如果有,里面字段’db_name’的值为SITE(其实还是有误差)的才是第四类,通过这种方式获取的列表页信息才是比较干净的数据。最后返回title和url数据。

详情页

1
2
3
4
5
6
7
8
9
10
def get_page_detail(url):
try:
response = requests.get(url=url, headers=headers)
if response.status_code == 200:
return response.text
return None
except RequestException as e:
print(e)
print("请求详情页出错")
return None

获取详情页response.text代码。

1
2
3
4
5
6
7
8
9
10
def parse_page_detail(html):
images_pattern = re.compile('gallery: JSON.parse\("(.*?)"\)')
result = re.search(images_pattern, html)
if result:
result_data = re.sub(r'\\', '', result.group(1))
data = json.loads(result_data)
images = [sub_image['url'] for sub_image in data['sub_images']]
for image in images:
download_image(image)
return images

通过正则,获取内容后,由于反斜杠‘\’的存在影响了我们的进一步操作,因此,有必要把反斜杠去掉,需要注意的是,这里显示的每一个反斜杠,实际上源字符串中都有两个反斜杠,还有一个反斜杠是用来转义的,故print是并不显示。

因此,我们要实际上要去掉的是两个连续的反斜杠,使用re.sbu(r’\’,’’,results)进行替换

处理后,获得的就是一个images的url的list列表(此处还调用了一个下载图片的函数,后续会介绍),然后返回。

数据库和图片下载代码等

数据库可以使用配置类,所以另建一个配置文件config.py

1
2
3
4
5
6
7
8
MONGO_URL = 'localhost'
MONGO_DB = 'toutiao'
MONGO_TABLE = 'toutiao'

GROUT_START = 1
GROUP_END = 40

KEYWORD = '街拍'

然后在使用mongo的时候调用

1
2
3
4
5
6
7
8
9
10
# 生成mongodb数据库对象
client = pymongo.MongoClient(MONGO_URL,connect=False)
db = client[MONGO_DB]


def save_to_mongo(result):
if db[MONGO_TABLE].insert(result):
print("存储到mongodb成功", result)
return True
return False

图片下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def download_image(url):
print("正在下载", url)
try:
response = requests.get(url=url, headers=headers)
if response.status_code == 200:
save_image(response.content)
return None
except RequestException as e:
print(e)
print("请求图片出错", url)
return None


def save_image(content):
file_path = '{0}/{1}.{2}'.format(os.getcwd(), md5(content).hexdigest(), 'jpg')
if not os.path.exists(file_path):
with open(file_path, 'wb') as f:
f.write(content)

下载图片使用md5计算名称,存到当前路径。

main函数

1
2
3
4
5
6
7
8
9
10
11
12
def main(offset):
html = get_page_index(offset, KEYWORD)
for item in parse_page_index(html):
print(item)
html = get_page_detail(item['url'])
if html:
images = parse_page_detail(html)
save_to_mongo({
'title': item['title'],
'url': item['url'],
'images': images
})

main函数参数为offset,keyword由配置文件传入。

main函数逻辑是,获取offset(偏移)后,获取这个json的数据并且解析出每一个item(dict类型,含有url和title),通过get_page_detail解析出每个网页的html,通过parse_page_detail解析出images,并且通过save_to_mongo存储到mongodb。

在调用parse_page_detail的时候就已经下载了图片。

运行函数

1
2
if __name__ == '__main__':
main(offset=20)

如果要多进程,使用:

1
2
3
4
if __name__ == '__main__':
groups = [x * 20 for x in range(GROUT_START, GROUP_END + 1)]
pool = Pool()
pool.map(main, groups)

groups为config.py中设置的1-40,groups实际上值为[20,40,60,…,400]。

-------------本文结束 感谢您的阅读-------------