天天看點

Python爬蟲之動态資料的采集

作者:梓羽Python

一、簡介

有時候我們在用 requests 抓取頁面的時候,得到的結果可能和在浏覽器中看到的不一樣:在浏覽器中可以看到正常顯示的頁面資料,但是使用 requests 得到的結果并沒有。這是因為 requests 擷取的都是原始的 HTML 文檔,而浏覽器中的頁面則是經過 JavaScript 處理資料後生成的結果,這些資料的來源有多種,可能是通過 Ajax 加載的,可能是包含在 HTML 文檔中的,也可能是經過 JavaScript 和特定算法計算後生成的。

對于第一種情況,資料加載是一種異步加載方式,原始的頁面最初不會包含某些資料,原始頁面加載完後,會再向伺服器請求某個接口擷取資料,然後資料才被處理進而呈現到網頁上,這其實就是發送了一個 Ajax 請求。

照 Web 發展的趨勢來看,這種形式的頁面越來越多。網頁的原始 HTML 文檔不會包含任何資料,資料都是通過 Ajax 統一加載後再呈現出來的,這樣在 Web 開發上可以做到前後端分離,而且降低伺服器直接渲染頁面帶來的壓力。

是以如果遇到這樣的頁面,直接利用 requests 等庫來抓取原始頁面,是無法擷取到有效資料的,這時需要分析網頁背景向接口發送的 Ajax 請求,如果可以用 requests 來模拟 Ajax 請求,那麼就可以成功抓取了。

1. 什麼是Ajax

Ajax,全稱為 Asynchronous JavaScript and XML,即異步的 JavaScript 和 XML。它不是一門程式設計語言,而是利用 JavaScript 在保證頁面不被重新整理、頁面連結不改變的情況下與伺服器交換資料并更新部分網頁的技術。

對于傳統的網頁,如果想更新其内容,那麼必須要重新整理整個頁面,但有了 Ajax,便可以在頁面不被全部重新整理的情況下更新其内容。在這個過程中,頁面實際上是在背景與伺服器進行了資料互動,擷取到資料之後,再利用 JavaScript 改變網頁,這樣網頁内容就會更新了。

2. 手寫Ajax接口服務

  • 架構簡介

Flask是一個基于Python并且依賴于Jinja2模闆引擎和Werkzeug WSGI 服務的一個微型架構 WSGI :Web Server Gateway Interface(WEB服務網關接口),定義了使用python編寫的web app與web server之間接口格式

  • 環境搭建
pip install flask           
Python爬蟲之動态資料的采集

3. 前端程式設計

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<style>

    td{
        width: 65px;
        text-align: center;
        font-size: 18px;
    }
</style>
<body>

<h1>hello</h1>

<!--
style  寫 CSS
HTML文法  标簽  table 表格
-->
<table style="margin: 0 auto; border: 1px solid; margin-top: 100px">

    <tr>
        <th>序号</th>
        <th>姓名</th>
        <th>年齡</th>
    </tr>

    <tbody>

        {% for i in data.data %}
    <tr>
        <td>{{ i.id }}</td>
        <td>{{ i.name }}</td>
        <td>{{ i.age }}</td>
    </tr>
        {% endfor %}

    </tbody>


</table>

</body>
</html>           

4.後端接口設計(Flask)

from flask import Flask,render_template,jsonify
# 執行個體
app = Flask(__name__)

@app.route('/')
def ff():
    return render_template('feifei.html')


@app.route('/xl')
def susu():
    data = []

    for i in range(1, 10):
        data.append(
            {'id': i, 'name': '魚' + str(i), 'age': 18 + i}
        )
    # 構造資料格式
    content = {
        'status': 0,
        'data': data
    }
    return jsonify(data=content)

@app.route('/api')
def index():
    data = []

    for i in range(1,7):
        data.append(
            {'id':i, 'name': '魚'+str(i), 'age':18+i}
        )

    # 構造資料格式
    content = {
        'status': 0,
        'data':data
    }
    print(content)
    # flask 架構 傳回資料給前端
    return render_template('index.html',data=content)

if __name__ == '__main__':
    app.run()           

二、基本原理

初步了解了 Ajax 之後,我們再來詳細了解它的基本原理。發送 Ajax 請求到網頁更新的這個過程可以簡單分為以下 3 步:

  • 發送請求
  • 解析内容
  • 渲染網頁
Python爬蟲之動态資料的采集

三、資料采集案例

1. Ajax 資料加載

Ajax 其實有其特殊的請求類型,其 Type 為 xhr,這就是一個 Ajax 請求。用滑鼠點選這個請求,可以檢視這個請求的詳細資訊。

示例網站:

https://danjuanapp.com/rank/performance
http://www.cninfo.com.cn/new/commonUrl?url=disclosure/list/notice#szseGem
https://cs.lianjia.com/           

2. 資料采集實戰

采集位址:https://cs.lianjia.com/

# encoding: utf-8

import requests
from loguru import logger
import json

class Spdier_data():

    def __init__(self):
        self.headers = {
            "Accept": "application/json, text/javascript, */*; q=0.01",
            "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6",
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "Pragma": "no-cache",
            "Referer": "https://cs.fang.lianjia.com/loupan/pg2/",
            "Sec-Fetch-Dest": "empty",
            "Sec-Fetch-Mode": "cors",
            "Sec-Fetch-Site": "same-origin",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36",
            "X-Requested-With": "XMLHttpRequest",
            "sec-ch-ua": "^\\^",
            "sec-ch-ua-mobile": "?0",
            "sec-ch-ua-platform": "^\\^Windows^^"
        }

    def http(self,url,params):
        res = requests.get(url,params=params,headers=self.headers)
        if res.status_code == 200:
            return res

    def get_data(self,url,params):
        response = self.http(url=url,params=params)
        items  = response.json()
        lists = items.get('data').get('list')
        for i in lists:
            item = {}
            item['address'] = i.get('address')
            item['city_name'] = i.get('city_name')
            item['decoration'] = i.get('decoration')
            item['district'] = i.get('district')
            item['title'] = i.get('title')
            item['show_price_info'] = i.get('show_price_info')
            logger.info(json.dumps(item))
            self.save_data(item)


    def save_data(self,data):
        with open('data.json','a',encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=4)
            f.write(',')

    def run(self):
        for i in range(1,3):
            url = "https://cs.fang.lianjia.com/loupan/pg{}/".format(str(i))
            params = {
                "_t": "1"
            }
            self.get_data(url,params)

if __name__ == '__main__':
    Spdier_data().run()           

四、資料去重算法

資料去重算法使用redis進行承載

redis 去重 集合類型,還有布隆算法。都是資料去重常用技術。

# encoding: utf-8

import requests
from base_request import Spiders
from lxml import etree
from loguru import logger
import time
import redis
client = redis.Redis()
import hashlib

class Crawl(Spiders):

    def __init__(self):
        self.url = 'https://36kr.com/information/web_news/latest/'
        self.maps = lambda x:x[0] if x else x

    def ma5_data(self,content):
        m = hashlib.md5()
        m.update(content.encode())
        return m.hexdigest()

    def crawl(self):
        res = self.fetch(self.url)
        html = etree.HTML(res.text)
        obj = html.xpath('//div[@class="information-flow-list"]/div')
        for i in obj:
            title = self.maps(i.xpath('.//p[@class="title-wrapper ellipsis-2"]/a/text()'))
            data_z = self.ma5_data(title) # 對資料進行壓縮  減少記憶體損耗
            tag = client.sadd('36k',data_z) # 傳回值 0 1
            if tag:
                # 表示資料可以繼續爬  對資料進行入庫   logger = print
                logger.info('可以入庫{}'.format(title))
            else:
                time.sleep(5)
                logger.info('休息5秒鐘')
                self.crawl()

    def save(self,data):
        with open('data.txt','a',encoding='utf-8') as x:
            x.write(data)
            x.write('\r\n')

    def run(self):
        while True:
            logger.info('開始啟動爬蟲')
            self.crawl()

if __name__ == '__main__':
    Crawl().run()