站點分析
首先,打開頭條,在搜索框輸入關鍵字之后,在返回的頁面中,勾選Perserve log,這玩意兒在頁面發生變化的時候,不會清除之前的交互信息.
在返回的response中,我們看不到常見的HTML代碼,所以初步判定,這個網站是通過ajax動態加載的.

切換到XHR過濾器,進一步查看.

發現隨着網頁的滾動,會產生類似這樣的的Ajax請求出來. 仔細查看內容,可以看到與網頁中條目對應的title和article_url.
所以初步思路,通過article_url字段先抓取文章條目
分析json數據,可以看到,這里有article_url
,另外,這次要抓取的是圖集形式的頁面,所以要注意下這個has_gallery
然后我們再來看具體的頁面
在具體頁面的html中,我們發現,圖片的所有鏈接直接在網頁源代碼中包含了,所以,我們直接拿到源碼,正則匹配一下就好了.

至此,頁面分析完成.
開工!
源碼及遇到的問題
代碼結構
方法定義
def get_page_index(offset, keyword):
獲取搜索結果索引頁面
def parse_page_index(html):
解析索引頁面,主要是解析json內容,所以需要用到json.loads方法
def get_page_detail(url):
用來獲取具體圖片的頁面,與索引頁獲取差不多
def parse_page_details(html, url):
解析具體圖集頁面
def save_to_mongo(result):
將標題,url等內容保存到mongoDB數據庫. 之所以使用mongoDB數據庫,因為mongoDB簡單,而且是K-V方式的存儲,對於字典類型很友好
def download_image(url):
下載圖片
def save_img(content):
保存圖片
def main(offset):
對以上各種方法的調用
需要的常量
MONGO_URL = 'localhost' # 數據庫位置
MONGO_DB = 'toutiao' # 數據庫名
MONGO_TABLE = 'toutiao'# 表名
GROUP_START = 1 # 循環起始值
GROUP_END = 20 # 循環結束值
KEY_WORD = '街拍' # 搜索關鍵字
關於在代碼中遇到的問題
01. 數據庫連接
第一次在python中使用數據庫,而且用的還是MongoDB. 使用之前引入 pymongo庫,數據庫連接的寫法比較簡單. 傳入url 然后在創建的client中直接指定數據庫名稱就可以了.
client = pymongo.MongoClient(MONGO_URL,connect=False)
db = client[MONGO_DB]
02.今日頭條的反爬蟲機制
今日頭條比較有意思,反爬蟲機制不是直接給個400的回應,而是返回一些錯誤的 無效的代碼或者json. 不明白是什么原理,是請求不對,還是怎么了. 所以針對今日頭條的反爬蟲機制,經過嘗試之后發現需要構造get的參數和請求頭.
而且今日頭條的請求頭中,需要帶上cookie信息. 不然返回的response還是有問題.
這里還要注意的就是cookie信息有時效問題,具體多長時間,我也沒搞明白,幾個小時應該是有的,所以在執行之前,cookie最好更新一下
同樣的在獲取詳情頁的時候也有這個問題存在. 而且還犯了一個被自己蠢哭的錯誤. headers沒有傳到requests方法中去.
def get_page_index(offset, keyword):
timestamp = int(time.time())
data = {
"aid": "24",
"app_name": "web_search",
"offset": offset,
"format": "json",
"keyword": keyword,
"autoload": "true",
"count": "20",
"en_qc": "1",
"cur_tab": "1",
"from": "search_tab",
# "pd": "synthesis",
"timestamp": timestamp
}
headers = {
# 這里小心cookie失效的問題
'cookie': 'tt_webid=6791640396613223949; WEATHER_CITY=%E5%8C%97%E4%BA%AC; tt_webid=6791640396613223949; csrftoken=4a29b1b1d9ecf8b5168f1955d2110f16; s_v_web_id=k6g11cxe_fWBnSuA7_RBx3_4Mo4_9a9z_XNI0WS8B9Fja; ttcid=3fdf0861117e48ac8b18940a5704991216; tt_scid=8Z.7-06X5KIZrlZF0PA9kgiudolF2L5j9bu9g6Pdm.4zcvNjlzQ1enH8qMQkYW8w9feb; __tasessionId=ngww6x1t11581323903383',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36'}
url = 'https://www.toutiao.com/api/search/content/?' + urlencode(data)
response = requests.get(url, headers=headers)
try:
if response.status_code == 200:
return response.text
return None
except RequestException:
print('Request failed!')
return None
03. json解碼遇到的問題
由於python和java轉移字符的區別(python通過''進行轉義,''本身不需要轉義),但是java需要\\
來進行轉義,也就是''本身還需要一個''來進行轉義.
但是python的json.loads()方法和print方法在輸出的時候都會對轉義字符進行解釋.
所以當初在parse_page_details()
這個方法中 json.loads()
報錯,說json格式錯誤找不到'"'. 但是print出來的時候,又是一個''的樣子.
后來在在debug的時候,看到了真實的json字符串的樣子
所以就需要對這個json字符串進行預處理,然后再使用json.loads()進行解碼.
eval(repr(result.group(1)).replace('\\\\', '\\'))
插一個小話題,那就是str()方法和repr()方法的區別. 首先兩者都是把對象轉換成字符串,而無論print方法還是str()方法調用的都是類中的__str__
而repr()方法調用的是__repr__
.
簡單來說,__str__
方法是為了滿足可讀性,會對輸出內容做可讀性處理. 比如去掉字符串兩端的引號或者自動解析''等. 但是__repr__
會盡量保留原始數據格式,滿足的是准確性需求. 所以這里,我們使用repr()方法拿到原始數據,然后將\\
替換為\
ps.\\\\
是兩個\
轉義了一下. 同理兩個斜杠是一個斜杠,因為也是轉義的.
然后就是eval方法是能把字符串轉換成對應的類型.
#字符串轉換成列表
>>>a = "[[1,2], [3,4], [5,6], [7,8], [9,0]]"
>>>type(a)
<type 'str'>
>>> b = eval(a)
>>> print b
[[1, 2], [3, 4], [5, 6], [7, 8], [9, 0]]
>>> type(b)
<type 'list'>
#字符串轉換成字典
>>> a = "{1: 'a', 2: 'b'}"
>>> type(a)<type 'str'
>>>> b = eval(a)
>>> print b
{1: 'a', 2: 'b'}>>> type(b)<type 'dict'>
理解repr()和eval()兩個方法之后,那上面的預處理代碼就好理解了,先通過repr()方法獲取原始字符串,然后替換,然后再給他轉換成可讀的字符串. 然后在用json.loads()解碼.
04. 關於response.text和response.content的區別
response.text 獲取文本值
response.content 獲取二進制內容
源代碼
import json
import os
import re
from hashlib import md5
from multiprocessing import Pool
from urllib.parse import urlencode
import pymongo
import requests
from bs4 import BeautifulSoup
from requests.exceptions import RequestException
from config import *
# mongodb 數據庫對象
# connext=False表示進程啟動的時候才進行連接
client = pymongo.MongoClient(MONGO_URL,connect=False)
db = client[MONGO_DB]
def get_page_index(offset, keyword):
data = {
"aid": "24",
"app_name": "web_search",
"offset": offset,
"format": "json",
"keyword": keyword,
"autoload": "true",
"count": "20",
"en_qc": "1",
"cur_tab": "1",
"from": "search_tab",
# "pd": "synthesis",
# "timestamp": "1581315480994"
}
headers = {
# 這里小心cookie失效的問題
'cookie': 'tt_webid=6791640396613223949; WEATHER_CITY=%E5%8C%97%E4%BA%AC; tt_webid=6791640396613223949; csrftoken=4a29b1b1d9ecf8b5168f1955d2110f16; s_v_web_id=k6g11cxe_fWBnSuA7_RBx3_4Mo4_9a9z_XNI0WS8B9Fja; ttcid=3fdf0861117e48ac8b18940a5704991216; tt_scid=8Z.7-06X5KIZrlZF0PA9kgiudolF2L5j9bu9g6Pdm.4zcvNjlzQ1enH8qMQkYW8w9feb; __tasessionId=ngww6x1t11581323903383',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36'}
url = 'https://www.toutiao.com/api/search/content/?' + urlencode(data)
response = requests.get(url, headers=headers)
try:
if response.status_code == 200:
return response.text
return None
except RequestException:
print('Request failed!')
return None
def parse_page_index(html):
data = json.loads(html)
# json.loads()方法會格式化結果,並生成一個字典類型
# print(data)
# print(type(data))
try:
if data and 'data' in data.keys():
for item in data.get('data'):
if item.get('has_gallery'):
yield item.get('article_url')
except TypeError:
pass
def get_page_detail(url):
headers = {
'cookie': 'tt_webid=6791640396613223949; WEATHER_CITY=%E5%8C%97%E4%BA%AC; tt_webid=6791640396613223949; csrftoken=4a29b1b1d9ecf8b5168f1955d2110f16; s_v_web_id=k6g11cxe_fWBnSuA7_RBx3_4Mo4_9a9z_XNI0WS8B9Fja; ttcid=3fdf0861117e48ac8b18940a5704991216; tt_scid=8Z.7-06X5KIZrlZF0PA9kgiudolF2L5j9bu9g6Pdm.4zcvNjlzQ1enH8qMQkYW8w9feb; __tasessionId=yix51k4j41581315307695',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36',
# ':scheme': 'https',
# 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
# 'accept-encoding': 'gzip, deflate, br',
# 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7'
}
try:
# 他媽的被自己蠢哭...忘了寫headers了,搞了一個多小時
response = requests.get(url, headers=headers)
# print(response.status_code)
if response.status_code == 200:
return response.text
return None
except RequestException:
print("請求詳情頁出錯!")
return None
def parse_page_details(html, url):
soup = BeautifulSoup(html, 'xml')
title = soup.select('title')[0].get_text()
# print(title)
img_pattern = re.compile('JSON.parse\("(.*?)"\),', re.S)
result = re.search(img_pattern, html)
if result:
# 這里注意一下雙斜杠的問題
data = json.loads(eval(repr(result.group(1)).replace('\\\\', '\\')))
if data and 'sub_images' in data.keys():
sub_images = data.get('sub_images')
images = [item.get('url') for item in sub_images]
for image in images: download_image(image)
return {
'title': title,
'url': url,
'images': images
}
def save_to_mongo(result):
if db[MONGO_TABLE].insert_one(result):
print('存儲到MongoDB成功', result)
return True
return False
def download_image(url):
print('正在下載', url)
try:
response = requests.get(url)
if response.status_code == 200:
save_img(response.content)
return None
except RequestException:
print('請求圖片出錯', url)
return None
def save_img(content):
file_path = '{0}/img_download/{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)
f.close()
def main(offset):
html = get_page_index(offset, KEY_WORD)
for url in parse_page_index(html):
html = get_page_detail(url)
if html:
result = parse_page_details(html, url)
if result: save_to_mongo(result)
if __name__ == '__main__':
groups = [x * 20 for x in range(GROUP_START, GROUP_END + 1)]
pool = Pool()
pool.map(main, groups)