?
筆者是頭條的深度使用者,經(jīng)常用頭條完成“看片”大業(yè)烂斋。若不信的話可以試試在頭條搜索街拍屹逛,返回的都是一道道靚麗的風(fēng)景線。
想把圖片存下來汛骂,該怎么辦呢罕模?我們可以用Python爬蟲啊。
1帘瞭、工具
Python3.5淑掌,Sublime Text,Windows 7
2蝶念、分析(第三步有完整代碼)
可以看到搜索結(jié)果默認(rèn)返回了 20 篇文章抛腕,當(dāng)頁面滾動(dòng)到底部時(shí)頭條通過 ajax 加載更多文章,瀏覽器按下 F12 打開調(diào)試工具(我的是 Chrome)媒殉,點(diǎn)擊 Network 選項(xiàng)担敌,嘗試加載更多的文章,可以看到相關(guān)的 http 請(qǐng)求:
?
此次返回Request URL:
來試試返回了什么
import json
from urllib import request
url = "http://www.toutiao.com/search_content/?offset=20&format=json&keyword=%E8%A1%97%E6%8B%8D&autoload=true&count=20&cur_tab=1"
with request.urlopen(url) as res:
? ? d = json.loads(res.read().decode())
? ? print(d)
?
發(fā)現(xiàn)我們需要的東西在'data'里廷蓉,打開一篇文章全封,來試試如何下載單篇圖片。
import json
from urllib import request
url = 'http://www.toutiao.com/a6314996711535444226/#p=1'
with request.urlopen(url) as res:
? ? soup = BeautifulSoup(res.read().decode(errors='ignore'), 'html.parser')
? ? article_main = soup.find('div', id='article-main')
? ? photo_list = [photo.get('src') for photo in article_main.find_all('img') if photo.get('src')]
? ? print(photo_list)
?
輸出
['http://p3.pstatp.com/large/159f00010b30d6736512', 'http://p1.pstatp.com/large/1534000488c40143b9ce', 'http://p3.pstatp.com/large/159d0001834ff61ccb8c', 'http://p1.pstatp.com/large/1534000488c1cd02b5ed']
首先用BeautifulSoup解析網(wǎng)頁,通過 find 方法找到 article-main 對(duì)應(yīng)的 div 塊刹悴,在該 div 塊下繼續(xù)使用 find_all 方法搜尋全部的 img 標(biāo)簽行楞,并提取其 src 屬性對(duì)應(yīng)的值,于是我們便獲得了該文章下全部圖片的 URL 列表颂跨。
接下來就是保存圖片敢伸。
photo_url = "http://p3.pstatp.com/large/159f00010b30d6736512"
photo_name = photo_url.rsplit('/', 1)[-1] + '.jpg'
with request.urlopen(photo_url) as res, open(photo_name, 'wb') as f:
? ? f.write(res.read())
基本步驟就是這么多了,整理下爬取流程:
指定查詢參數(shù)恒削,向http://www.toutiao.com/search_content/提交我們的查詢請(qǐng)求池颈。
從返回的數(shù)據(jù)(JSON 格式)中解析出全部文章的 URL,分別向這些文章發(fā)送請(qǐng)求钓丰。
從返回的數(shù)據(jù)(HTML 格式)提取出文章的標(biāo)題和全部圖片鏈接躯砰。
再分別向這些圖片鏈接發(fā)送請(qǐng)求,將返回的圖片輸入保存到本地(E:\jiepai)携丁。
修改查詢參數(shù)琢歇,以使服務(wù)器返回新的文章數(shù)據(jù),繼續(xù)第一步梦鉴。
3李茫、完整代碼
import re
import json
import time
import random
from pathlib import Path
from urllib import parse
from urllib import error
from urllib import request
from datetime import datetime
from http.client import IncompleteRead
from socket import timeout as socket_timeout
from bs4 import BeautifulSoup
def _get_timestamp():
? ? """
? ? 向 http://www.toutiao.com/search_content/ 發(fā)送的請(qǐng)求的參數(shù)包含一個(gè)時(shí)間戳,
? ? 該函數(shù)獲取當(dāng)前時(shí)間戳肥橙,并格式化成頭條接收的格式魄宏。格式為 datetime.today() 返回
? ? 的值去掉小數(shù)點(diǎn)后取第一位到倒數(shù)第三位的數(shù)字。
? ? """
? ? row_timestamp = str(datetime.timestamp(datetime.today()))
? ? return row_timestamp.replace('.', '')[:-3]
def _create_dir(name):
? ? """
? ? 根據(jù)傳入的目錄名創(chuàng)建一個(gè)目錄存筏,這里用到了 python3.4 引入的 pathlib 庫宠互。
? ? """
? ? directory = Path(name)
? ? if not directory.exists():
? ? ? ? directory.mkdir()
? ? return directory
def _get_query_string(data):
? ? """
? ? 將查詢參數(shù)編碼為 url,例如:
? ? data = {
? ? ? ? ? ? 'offset': offset,
? ? ? ? ? ? 'format': 'json',
? ? ? ? ? ? 'keyword': '街拍',
? ? ? ? ? ? 'autoload': 'true',
? ? ? ? ? ? 'count': 20,
? ? ? ? ? ? '_': 1480675595492
? ? }
? ? 則返回的值為:
? ? ?offset=20&format=json&keyword=%E8%A1%97%E6%8B%8D&autoload=true&count=20&_=1480675595492"
? ? """
? ? return parse.urlencode(data)
def get_article_urls(req, timeout=10):
? ? with request.urlopen(req, timeout=timeout) as res:
? ? ? ? d = json.loads(res.read().decode()).get('data')
? ? ? ? if d is None:
? ? ? ? ? ? print("數(shù)據(jù)全部請(qǐng)求完畢...")
? ? ? ? ? ? return
? ? ? ? urls = [article.get('article_url') for article in d if article.get('article_url')]
? ? ? ? return urls
def get_photo_urls(req, timeout=10):
? ? with request.urlopen(req, timeout=timeout) as res:
? ? ? ? # 這里 decode 默認(rèn)為 utf-8 編碼椭坚,但返回的內(nèi)容中含有部分非 utf-8 的內(nèi)容予跌,會(huì)導(dǎo)致解碼失敗
? ? ? ? # 所以我們使用 ignore 忽略這部分內(nèi)容
? ? ? ? soup = BeautifulSoup(res.read().decode(errors='ignore'), 'html.parser')
? ? ? ? article_main = soup.find('div', id='article-main')
? ? ? ? if not article_main:
? ? ? ? ? ? print("無法定位到文章主體...")
? ? ? ? ? ? return
? ? ? ? heading = article_main.h1.string
? ? ? ? if '街拍' not in heading:
? ? ? ? ? ? print("這不是街拍的文章!I凭ァ券册!")
? ? ? ? ? ? return
? ? ? ? img_list = [img.get('src') for img in article_main.find_all('img') if img.get('src')]
? ? ? ? return heading, img_list
def save_photo(photo_url, save_dir, timeout=10):
? ? photo_name = photo_url.rsplit('/', 1)[-1] + '.jpg'
? ? # 這是 pathlib 的特殊操作,其作用是將 save_dir 和 photo_name 拼成一個(gè)完整的路徑巾表。例如:
? ? # save_dir = 'E:\jiepai'
? ? # photo_name = '11125841455748.jpg'
? ? # 則 save_path = 'E:\jiepai\11125841455748.jpg'
? ? save_path = save_dir / photo_name
? ? with request.urlopen(photo_url, timeout=timeout) as res, save_path.open('wb') as f:
? ? ? ? f.write(res.read())
? ? ? ? print('已下載圖片:{dir_name}/{photo_name}汁掠,請(qǐng)求的 URL 為:{url}'
? ? ? ? ? ? ? .format(dir_name=dir_name, photo_name=photo_name, url=a_url))
if __name__ == '__main__':
? ? ongoing = True
? ? offset = 0? # 請(qǐng)求的偏移量,每次累加 20
? ? root_dir = _create_dir('E:\jiepai')? # 保存圖片的根目錄
? ? request_headers = {
? ? ? ? 'Referer': 'http://www.toutiao.com/search/?keyword=%E8%A1%97%E6%8B%8D',
? ? ? ? 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36'
? ? }
? ? while ongoing:
? ? ? ? timestamp = _get_timestamp()
? ? ? ? query_data = {
? ? ? ? ? ? 'offset': offset,
? ? ? ? ? ? 'format': 'json',
? ? ? ? ? ? 'keyword': '街拍',
? ? ? ? ? ? 'autoload': 'true',
? ? ? ? ? ? 'count': 20,? # 每次返回 20 篇文章
? ? ? ? ? ? '_': timestamp
? ? ? ? }
? ? ? ? query_url = 'http://www.toutiao.com/search_content/' + '?' + _get_query_string(query_data)
? ? ? ? article_req = request.Request(query_url, headers=request_headers)
? ? ? ? article_urls = get_article_urls(article_req)
? ? ? ? # 如果不再返回?cái)?shù)據(jù)集币,說明全部數(shù)據(jù)已經(jīng)請(qǐng)求完畢考阱,跳出循環(huán)
? ? ? ? if article_urls is None:
? ? ? ? ? ? break
? ? ? ? # 開始向每篇文章發(fā)送請(qǐng)求
? ? ? ? for a_url in article_urls:
? ? ? ? ? ? # 請(qǐng)求文章時(shí)可能返回兩個(gè)異常,一個(gè)是連接超時(shí) socket_timeout鞠苟,
? ? ? ? ? ? # 另一個(gè)是 HTTPError乞榨,例如頁面不存在
? ? ? ? ? ? # 連接超時(shí)我們便休息一下秽之,HTTPError 便直接跳過。
? ? ? ? ? ? try:
? ? ? ? ? ? ? ? photo_req = request.Request(a_url, headers=request_headers)
? ? ? ? ? ? ? ? photo_urls = get_photo_urls(photo_req)
? ? ? ? ? ? ? ? # 文章中沒有圖片吃既?跳到下一篇文章
? ? ? ? ? ? ? ? if photo_urls is None:
? ? ? ? ? ? ? ? ? ? continue
? ? ? ? ? ? ? ? article_heading, photo_urls = photo_urls
? ? ? ? ? ? ? ? # 這里使用文章的標(biāo)題作為保存這篇文章全部圖片的目錄考榨。
? ? ? ? ? ? ? ? # 過濾掉了標(biāo)題中在 windows 下無法作為目錄名的特殊字符。
? ? ? ? ? ? ? ? dir_name = re.sub(r'[\\/:*?"<>|]', '', article_heading)
? ? ? ? ? ? ? ? download_dir = _create_dir(root_dir / dir_name)
? ? ? ? ? ? ? ? # 開始下載文章中的圖片
? ? ? ? ? ? ? ? for p_url in photo_urls:
? ? ? ? ? ? ? ? ? ? # 由于圖片數(shù)據(jù)以分段形式返回鹦倚,在接收數(shù)據(jù)時(shí)可能拋出 IncompleteRead 異常
? ? ? ? ? ? ? ? ? ? try:
? ? ? ? ? ? ? ? ? ? ? ? save_photo(p_url, save_dir=download_dir)
? ? ? ? ? ? ? ? ? ? except IncompleteRead as e:
? ? ? ? ? ? ? ? ? ? ? ? print(e)
? ? ? ? ? ? ? ? ? ? ? ? continue
? ? ? ? ? ? except socket_timeout:
? ? ? ? ? ? ? ? print("連接超時(shí)了河质,休息一下...")
? ? ? ? ? ? ? ? time.sleep(random.randint(15, 25))
? ? ? ? ? ? ? ? continue
? ? ? ? ? ? except error.HTTPError:
? ? ? ? ? ? ? ? continue
? ? ? ? # 一次請(qǐng)求處理完畢,將偏移量加 20震叙,繼續(xù)獲取新的 20 篇文章掀鹅。
? ? ? ? offset += 20
同理,只需修改代碼媒楼,就可以下載想要的關(guān)鍵詞乐尊,自己動(dòng)手,想啥有啥划址。
?