1. 程式人生 > >分析Ajax爬取今日頭條街拍美圖-崔慶才思路

分析Ajax爬取今日頭條街拍美圖-崔慶才思路

    • 站點分析
    • 原始碼及遇到的問題
      • 程式碼結構
        • 方法定義
        • 需要的常量
      • 關於在程式碼中遇到的問題
        • 01. 資料庫連線
        • 02.今日頭條的反爬蟲機制
        • 03. json解碼遇到的問題
        • 04. 關於response.text和response.content的區別
      • 原始碼

站點分析

首先,開啟頭條,在搜尋框輸入關鍵字之後,在返回的頁面中,勾選Perserve log,這玩意兒在頁面發生變化的時候,不會清除之前的互動資訊.

在返回的response中,我們看不到常見的HTML程式碼,所以初步判定,這個網站是通過ajax動態載入的.


pic-1581682361199.png

切換到XHR過濾器,進一步檢視.


pic-1581682361200.png

發現隨著網頁的滾動,會產生類似這樣的的Ajax請求出來. 仔細檢視內容,可以看到與網頁中條目對應的title和article_url.

所以初步思路,通過article_url欄位先抓取文章條目

分析json資料,可以看到,這裡有article_url,另外,這次要抓取的是圖集形式的頁面,所以要注意下這個has_gallery

然後我們再來看具體的頁面

在具體頁面的html中,我們發現,圖片的所有連結直接在網頁原始碼中包含了,所以,我們直接拿到原始碼,正則匹配一下就好了.


pic-1581682361200.png

至此,頁面分析完成.

開工!

原始碼及遇到的問題

程式碼結構

方法定義

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)