亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定
2. 如何訪問 RabbitMQ 消息監控平臺 ?

什么是 RabbitMQ 消息監控平臺RabbitMQ 消息監控平臺,即用來監控 RabbitMQ 中所有的消息所處的狀態,以及當前 RabbitMQ Server 的狀態的一個網絡平臺,為 RabbitMQ 內嵌的自我監控平臺,在安裝好了 RabbitMQ 之后就可以通過特定的路徑來訪問這一消息監控平臺。出于方便考慮,后續我們都將 RabbitMQ 消息監控平臺,稱為 RabbitMQ 消息管控臺。在 RabbitMQ 消息管控臺中,可以對我們應用程序中,所有的消息進行監控,包括消息是否已經被發送、消息是否已經被接收,以及 RabbitMQ Server 中,交換機、頻道、消息隊列的狀態等,經過對這些關鍵指標進行監控,我們可以很好地掌握我們應用程序中的消息在 RabbitMQ Server 中的狀態,以及我們的 RabbitMQ Server 節點工作是否正常等關鍵性能指標。如何訪問 RabbitMQ 消息監控平臺 ?在介紹如何安裝 RabbitMQ 小節內容中,已經為大家介紹了如何來驗證我們的 RabbitMQ 是否已經安裝成功了,而這一驗證手段正是我們訪問 RabbitMQ 消息監控平臺的方法,這里我們來簡單回顧一下。在將 RabbitMQ 安裝成功之后,需要我們啟動 RabbitMQ Server 服務,該服務會占用我們的 5672 端口,然后對外暴露 15672 端口,來供我們訪問,所以,要想訪問 RabbitMQ 消息管控臺,我們只需要輸入 RabbitMQ Server 所在節點的 ip 地址,以及 15672 端口的組合方式就可以了。以本地為例,訪問 RabbitMQ 消息管控臺的地址如下:http://localhost:15672

2.1 什么是 HaProxy 組件?

什么是 HaProxy 組件呢?我們可以像理解 KeepAlived 組件那樣,去理解 HaProxy 組件。HaProxy 這一名詞和 KeepAlived 名詞一樣,也是一個組合詞,我們拆開來看,Ha 其實是 High Availability 簡稱,取的是兩個單詞的首字符,即 HA ,中文含義就是我們常說的高可用。而 proxy 就比較簡單了,proxy 翻譯過來具有代理的含義,代理我們可以理解為我們平常生活中的第三方辦事機構,就是我們自己的事情交給第三方去做,等第三方完成我們交代的任務后,會給我們一個通知,HaProxy 中的 proxy 就是這個意思。在清楚了 HaProxy 各組成名詞之后,我們就不難理解到底什么是 HaProxy 了。HaProxy 翻譯過來其實指的就是一種高可用的代理工具,或者可以叫做一種軟件。即 HaProxy 就是一種為集群提供高可用支持與服務的,一種高可用代理工具,其可以管理集群間的通信方式,可以對集群進行健康檢查,甚至也可以對集群進行全方位的監控,說白了,HaProxy 就是一種輔助集群實現高可用、對集群進行監控、對集群進行健康檢查的一種組件或軟件。而實現這種高可用,以及集群監控和集群健康檢查的手段,則是通過 HaProxy 中的 Proxy 代理實現的,我們可以配置采用 tcp 還是 http 代理的方式,這點同學們簡單了解即可。

3. 通過 XML 實現一個登錄界面

在編寫代碼之前,我們先來一起來分析分析這個頁面,一步一步的把它拆分成一個網格:整個頁面占滿全屏,我們知道直接把 GridView 的長寬設置成match_parent即可從左到右一共分為 3 列:賬號/密碼下劃線輸入框提交第一行和第二行占滿全列,“提交”在最后一行的最后一列基本上按照以上思路就可以把整個頁面按照網格分割好,接下來就可以開始 xml 代碼的編寫了:<?xml version="1.0" encoding="utf-8"?><GridLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:columnCount="3" android:useDefaultMargins="true"> <TextView android:layout_columnSpan="3" android:layout_gravity="center_horizontal" android:text="用戶登錄" android:textSize="32sp" /> <TextView android:layout_columnSpan="3" android:layout_gravity="left" android:text="請輸入您的賬號密碼:" android:textSize="20sp" /> <TextView android:layout_gravity="right" android:text="賬號:" /> <EditText android:ems="10" /> <TextView android:layout_column="0" android:layout_gravity="right" android:text="密碼:" /> <EditText android:ems="8" /> <Button android:layout_row="3" android:layout_column="2" android:layout_gravity="bottom" android:text="提交" /></GridLayout>以上代碼有幾點需要注意:我們僅僅指定了列數,系統可以根據列數計算出行數;GridLayout 內部的 View 在沒有設置具體的行列序號的時候,默認按照順序依次從左往右、從上往下依次排列;GridLayout 非常智能,為了向大家展示它的優勢,本代碼中也省略了大量的屬性設置,全都交給系統自行計算。不過在實際使用中建議還是標注出來,這樣可讀性更高。

2. 異常處理原則

異常是程序運行過程中不可避免的問題。異常出現的原因很多,但不管怎樣,都需要提前預知或者當異常發生后采取相應的處理措施。異常的處理原則是:能預知的盡可能在邏輯層面提前制止。如用戶注冊時,要求登錄名是唯一的,可先檢查數據庫是否存在同名用戶名后,再進行添加操作;以一種友好的方式告知使用者出錯的原因;采用多層體系結構的項目中,建議異常由下逐層向上拋出,一直到達應用層面;使用日志記錄功能把異常信息記錄在日志文件中,便于開發者分析。如下面的控制器方法:@Controllerpublic class ExceptionAction {@RequestMapping("/exception01")public String exception01(@RequestParam("userName") String userName) { return "exception";}}在瀏覽器中輸入:http://localhost:8888/sm-demo/exception01 ,頁面中會出現錯誤提示。這個原因是 @RequestParam(“userName”) 注解在默認情況下,要求請求包中一定要有 userName 這個參數。顯然,頁面中顯示出來的錯誤信息是不友好的。所謂的異常處理,并不能完全阻止異常的發生。而是把異常信息對外、對內做一個封裝,換一個淺白的、直接的、非專業的方式告訴使用者。對于前面的異常解決方案,可以在 @RequestParam(value = “userName”,required = false) 中添加一個 required = false 的設置。這是一種最理想的異常解決方案。

1. 對接 Eclipse 使用

Eclipse 作為一個老牌的 Java 開發工具,在很長一段時間都是市面上最流行的?,F在,我們就介紹一下在 Eclipse 中如何使用 Maven 來構建項目。首先,我們來安裝 m2eclipse 插件。在菜單中選擇:Help >> Install New Software,并在彈出的窗口中輸入 m2eclipse 插件的地址 http://download.eclipse.org/technology/m2e/releases;安裝插件的過程沒什么太多差異,一路 Next 即可完成安裝;插件安裝完成后,我們配置已經安裝好的 Maven ,菜單路徑:Window >> Preferences,在彈窗中選擇Maven >> Installations;并且在 User Settings 中配置對應的 setting.xml 文件和本地倉庫;配置好 Maven 之后,我們來構建項目。在 Eclipse 中使用 Maven 一般有兩種方式,一種是使用命令行的形式,一種是在 Configuration 中進行配置,下面我們來介紹一下這兩種使用方式。使用命令行構建。這種構建方式其實和我們使用 cmd 來構建是一樣的。菜單選擇Window >> Show view >> Other,在彈出的窗口中選擇 Terminal。在控制臺中即可執行任何 Maven 相關的命令;配置 Configuration。這種方式直接在項目的根目錄上單擊右鍵,選擇Run As >> Maven build,在彈出的窗口中可以輸入對應的目標,以及要激活的 profile ,選擇好之后,點擊下方的 run 按鈕即可執行。兩種方式本質上沒有什么差別,常用的操作可以配置到 Configuration 中,重復使用起來更方便,如果要靈活使用的話,命令行應該是更好的選擇。

2.1 Session 的相關配置

由于 Session 的數據是保存在服務器端的,所以很多工作是需要在服務器端來完成的,所以 Django 中 Session 的操作相比 Cookie 操作會略顯復雜。首先需要介紹 Django 中和 Session 相關的配置,同樣是在 settings.py 文件中:啟用 Session:需要在 MIDDLEWARE 值中添加相應的 Session 中間件,去對 Session 攔截和處理。另外,還需要再INSTALLED_APPS 中注冊 Session 應用。Django 中默認是有這個配置的。MIDDLEWARE = [ # ... 'django.contrib.sessions.middleware.SessionMiddleware', # ...]INSTALLED_APPS = [ # 啟用 sessions 應用 'django.contrib.sessions',]配置 Session 引擎:主要是配置 Session 保存方式,比如數據庫保存、內存保存、文件系統保存等。# 數據庫SessionSESSION_ENGINE = 'django.contrib.sessions.backends.db' # 默認引擎# 緩存SessionSESSION_ENGINE = 'django.contrib.sessions.backends.cache' # 使用的緩存別名(默認內存緩存,也可以是memcache),此處別名依賴緩存的設置SESSION_CACHE_ALIAS = 'default' # 文件SessionSESSION_ENGINE = 'django.contrib.sessions.backends.file' # 緩存文件路徑,如果為None,則使用tempfile模塊獲取一個臨時地址tempfile.gettempdir() SESSION_FILE_PATH = None # 緩存+數據庫SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' # 加密Cookie SessionSESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' 其他配置如下:# Session的cookie保存在瀏覽器上時的key,即:sessionid=隨機字符串(默認)SESSION_COOKIE_NAME = "sessionid" # Session的cookie保存的路徑(默認)SESSION_COOKIE_PATH = "/" # Session的cookie保存的域名(默認)SESSION_COOKIE_DOMAIN = None # 是否Https傳輸cookie(默認)SESSION_COOKIE_SECURE = False # 是否Session的cookie只支持http傳輸(默認)SESSION_COOKIE_HTTPONLY = True # Session的cookie失效日期(2周)(默認)SESSION_COOKIE_AGE = 1209600 # 是否關閉瀏覽器使得Session過期(默認)SESSION_EXPIRE_AT_BROWSER_CLOSE = False # 是否每次請求都保存Session,默認修改之后才保存(默認)SESSION_SAVE_EVERY_REQUEST = False 關于上述這些未出現在 settings.py 中的配置,默認的值都會在 django/conf/global_settings.py 中找到,如下圖所示:

2.4 服務消費者測試

在進行服務消費者測試之前,我們可以多啟動幾個服務提供者實例來模擬集群中的機器。在 IDEA 中使用快捷鍵 alt + 8 呼出 Services 面板,右鍵點擊 Services 面板中的 ZookeeperProviderApplication ,選擇 copy Configuration 復制我們的服務提供者,在彈出的 Edit Configuration 界面中,修改實例名稱,添加 VM options 參數 -Dserver.port=8091 來修改啟動端口號為 8091 ,點擊 OK 確認。在這里我們復制了兩個服務提供者,也就是說一共有 3 個服務提供者來提供服務。保持 Zookeeper 服務端的啟動,然后依次啟動 3 個服務提供者,觀察控制臺,我們可以看到 3 個服務提供者都輸出了服務上線的信息:>>> 服務提供者連接 Zookeeper ...>>> 本服務已上線服務提供者啟動完畢,接下來我們就可以啟動服務消費者來進行測試了,啟動服務消費者,觀察控制臺:>>> 服務消費者連接 Zookeeper ...>>> CuratorCacheListener 初始化我們可以看到,服務消費者連接上了 Zookeeper 服務器,并且開啟了監聽,接下來我們就可以訪問服務消費者的 RESTful 接口來測試了,訪問 http://localhost:9090/consumer/callMethod ,查看瀏覽器內容:調用了服務提供者 192.168.0.102:8090 的方法刷新頁面,再次訪問:調用了服務提供者 192.168.0.102:8091 的方法再次刷新頁面進行訪問:調用了服務提供者 192.168.0.102:8092 的方法我們可以發現,服務消費者依次調用了 8090 ,8091 ,8092 這 3 個服務提供者的方法,并且實現了輪詢的負載均衡策略。測試成功后,我們就可以模擬對集群管理的各種場景了。

2. CSRF 攻擊原理

我們用一個實例演示「CSRF」攻擊的過程。假設我們登陸了一個銀行網站(bank.example.com),這個網站的作用是實現跨行轉賬的表單提交,通常情況下,我們會生成如下一個 Form 表單。<form method="post" action="/transfer"> <!-- 匯款金額 --> <input type="text" name="amount"/> <!-- 匯款路由號 --> <input type="text" name="routingNumber"/> <!-- 匯款賬戶 --> <input type="text" name="account"/> <input type="submit" value="提交"/></form>那我們發出的「post」請求格式可能如下:POST /transfer HTTP/1.1Host: bank.example.comCookie: JSESSIONID=randomidContent-Type: application/x-www-form-urlencodedamount=100.00&routingNumber=1234&account=9876此時,如果我們未登出,并且訪問了其他惡意網站,并且其他惡意網站同樣包含了可提交的表單,表單形式如下:<form method="post" action="https://bank.example.com/transfer"> <!-- 隱藏項不可見,轉賬金額,固定 100 元 --> <input type="hidden" name="amount" value="100.00"/> <!-- 隱藏項不可見,轉賬路由碼 --> <input type="hidden" name="routingNumber" value="evilsRoutingNumber"/> <!-- 隱藏項不可見,轉賬賬戶 --> <input type="hidden" name="account" value="evilsAccountNumber"/> <!-- 可見 --> <input type="submit" value="快來點我!"/></form>當我們很好奇,點擊了「快來點我」按鈕時,我們會觸發轉賬請求,并將錢匯款到一個未知賬戶里。在這個過程中,雖然惡意網站并不知道我們的「Cookies」值,但是由于未登出,我們和銀行網站之間的 Cookies 還在,所以當我們再次發起請求時,該 Cookies 依然有效,這使得不知不覺被觸發的轉賬請求同樣有效。除此之外,如果惡意網站使用 JS 腳本自動提交表單的話,用戶可能沒有任何被攻擊的感覺。

4. Spring Boot 集成 RabbitMQ 消息通信中間件是否成功的必要性測試

無論使用哪一種集成方式,驗證 RabbitMQ 集成成功與否的方式都是類似下方的內容:當我們的 Maven 包管理工具解析完成 RabbitMQ 的依賴之后,此時,需要運行我們的 SpringBoot 項目,在項目啟動完成之后,我們可以在本地瀏覽器地址欄中,輸入以下地址:http://localhost:15672如果看到 RabbitMQ 提示的登錄信息框,如下圖所示,則表示我們已經成功把 RabbitMQ 消息通信中間件集成到了 SpringBoot 框架中去了。RabbitMQ 內置的默認賬號和密碼都是 guest ,我們可以登錄進去看下,如下圖所示:Tips:1.各位同學請務必按照老師要求的內容來安裝 RabbitMQ-Server ,當然,如果你足夠清楚不同的安裝包中的內容,那么,你可以通過下載 Binary 形式的完全安裝包進行安裝 RabbitMQ ,否則,請使用老師提供的安裝包進行安裝。2.在安裝 RabbitMQ 之前,請確保 erlang 語言支持庫已經成功安裝,如果 erlang 語言支持庫沒有安裝成功,請不要安裝 RabbitMQ ,如果已經在錯誤的環境安裝 RabbitMQ 導致報錯,那么請完全刪除掉已經安裝的 RabbitMQ 內容,從頭開始安裝。3.請確保下載的 erlang 語言支持庫的版本為 V23.0 最新版本,否則可能會導致 RabbitMQ 無法安裝成功。4.由于 RabbitMQ 是基于 AMQP 協議的(后續會介紹),所以,Maven 依賴的名稱為 amqp-client ,即 AMQP 客戶端,這一點,同學們簡單了解即可。5.在啟動項目之前,請先啟動自己本地電腦中的 RabbitMQ 服務,否則,項目無法啟動。

1. 前言

TCP 的英文全稱是 Transmission Control Protocol,翻譯成中文叫做傳輸控制協議,它是 TCP/IP 協議族中非常重要的一個傳輸層協議。TCP 是一個面向連接的、面向字節流的、可靠的傳輸層協議,有丟包重傳機制、有流控機制、有擁塞控制機制。TCP 保證數據包的順序,并且對重復包進行過濾。相比不可靠傳輸協議 UDP,TCP 完全是相反的。對于可靠性要求很高的應用場景來說,選擇可靠 TCP 作為傳輸層協議肯定是正確的。例如,著名的 HTTP 協議和 FTP 協議都是采用 TCP 進行傳輸。當然 TCP 為了保證傳輸的可靠性,引入了非常復雜的保障機制,比如:TCP 連接建立時的三次握手和連接關閉時的四次揮手機制,滑動窗口機制,發送流控機制,慢啟動和擁塞避免機制等。當然,操作系統的網絡協議棧已經實現了這些復雜的機制,本小節主要是介紹通過 Java 語言編寫 TCP 客戶端、服務器程序的方法。編寫 TCP 客戶端、服務器程序主要分為如下幾個步驟:創建客戶端 Socket,連接到某個服務器監聽的端口,需要指定服務器監聽的 host 和 port。host 可以是 IP 地址,也可以是域名。創建服務端 Socket,綁定到一個固定的服務端口,監聽客戶端的連接請求??蛻舳税l起連接請求,完成三次握手過程。TCP 連接建立成功后,雙方進行數據流交互。數據流交互完成后,關閉連接。

4. 獲取用戶的輸入信息

TextView 的功能只是作為信息輸出展示給用戶,而 EditText 除了可以展示信息之外,還需要從用戶那里得到輸入的內容,我們可以通過getText().toString()獲取到 EditText 的輸入信息。修改一下布局文件,添加一個 button 來觸發獲取的時機:<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="20dp"> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:text="確定" /> <EditText android:id="@+id/input_password" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginRight="10dp" android:layout_toLeftOf="@id/button" android:hint="請輸入密碼" android:imeOptions="actionDone" android:inputType="textPassword" android:maxLines="5" android:textSize="20sp" /></RelativeLayout>我們在第一個示例代碼基礎之上,加了一個 Button,并放在了一個 RelativeLayout 中(大家可以思考一下這里為什么要用 RelativeLayout,如果用 LinearLayout 能實現嗎?),我們希望用 Button 作為獲取 EditText 內容的觸發事件,當點擊 Button 的時候,我們獲取用戶在 EditText 中輸入的內容,并打印到屏幕上驗證結果。相關 Java 代碼如下:final EditText editText = findViewById(R.id.input_password); Button button = findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(MainActivity.this, editText.getText(), Toast.LENGTH_SHORT).show(); } });首先我們根據 ID 拿到 EditText 和 Button 的實例,接著對 Button 設置一個監聽器(關于 Button 監聽器的使用,如果不清楚可以參考后續 Button 章節,不是本節重點),在監聽器中我們獲取 EditText 的內容,并通過 Toast 打印到屏幕,效果如下:擴展:其實getText()不是 EditText 特有的,TextView 也可以通過getText()拿到顯示的內容,只不過 TextView 的內容是由我們設置上去的,所以需要去獲取的場景比較少,用的也相對少。

4.6 添加 MyBatis 映射文件

編寫數據訪問層接口之后,MyBatis 需要知道,如何將接口方法及參數轉換為 SQL 語句,以及 SQL 語句執行結果如何轉換為對象。這些都是通過映射文件描述的, MyBatis 映射文件就是描述對象 - 關系映射的配置文件。首先我們通過 application.properties 指定映射文件的位置:實例:# 指定MyBatis配置文件位置mybatis.mapper-locations=classpath:mapper/*.xml然后在 resources/mapper 目錄下新建 GoodsMapper.xml 文件,該文件就是 goods 表對應的映射文件,內容如下:實例:<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><!-- 本映射文件對應GoodsDao接口 --><mapper namespace="com.imooc.springbootmybatis.GoodsDao"> <!-- 對應GoodsDao中的insert方法 --> <insert id="insert" parameterType="com.imooc.springbootmybatis.GoodsDo"> insert into goods (name,price,pic) values (#{name},#{price},#{pic}) </insert> <!-- 對應GoodsDao中的delete方法 --> <delete id="delete" parameterType="java.lang.Long"> delete from goods where id=#{id} </delete> <!-- 對應GoodsDao中的update方法 --> <update id="update" parameterType="com.imooc.springbootmybatis.GoodsDo"> update goods set name=#{name},price=#{price},pic=#{pic} where id=#{id} </update> <!-- 對應GoodsDao中的selectOne方法 --> <select id="selectOne" resultMap="resultMapBase" parameterType="java.lang.Long"> select <include refid="sqlBase" /> from goods where id = #{id} </select> <!-- 對應GoodsDao中的selectAll方法 --> <select id="selectAll" resultMap="resultMapBase"> select <include refid="sqlBase" /> from goods </select> <!-- 可復用的sql模板 --> <sql id="sqlBase"> id,name,price,pic </sql> <!-- 保存SQL語句查詢結果與實體類屬性的映射 --> <resultMap id="resultMapBase" type="com.imooc.springbootmybatis.GoodsDo"> <id column="id" property="id" /> <result column="name" property="name" /> <result column="price" property="price" /> <result column="pic" property="pic" /> </resultMap></mapper>

2. 實戰:使用 flask 調用 redis 容器應用

接下來我們將通過一個flask web應用,實現主頁訪問計數功能,我們使用 redis 服務幫助我們實現計數統計功能。這個 redis 服務運行在我們的 Docker 容器中。Tips: 這里使用 flask 只是想借助一個簡單的 web 框架來演示使用 docker 容器運行的 redis,不了解也沒有關系,這里只是一個小例子。首先,我們在 CentOS 中安裝 Python3 和flask框架,以及 Python 的 redis 客戶端庫。# 安裝python3sudo dnf install -y python3# 安裝flask與redis python客戶端pip3 install redis flask --user接下來,將下面的代碼保存到~/test/app.pyimport flaskfrom flask import Flaskapp = Flask(__name__)from redis import StrictRedisfrom redis import ConnectionPool# 指定redis服務地址REDIS_HOST = os.getenv('REDIS_HOST','127.0.0.1')# 指定redis端口號REDIS_PORT = os.getenv('REDIS_PORT', '6379')# 指定redis的數據庫REDIS_DB = os.getenv('REDIS_DB', '0')# 指定redis的密碼REDIS_PASSWORD = os.getenv('REDIS_PASSWORD', '12345678')class Redis: def __init__(self): self.cli = None def connect(self): pool = ConnectionPool(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, password=REDIS_PASSWORD) return StrictRedis(connection_pool=pool) def add_pv(self): self.connection.incr('pv', 1) def get_pv(self): count = self.connection.get('pv') return int(count) @property def connection(self): if self.cli: return self.cli else: self.cli = self.connect() return self.cliredis = Redis()@app.route('/')def index(): redis.add_pv() return "<h1>Hello World, 本頁已訪問{}次。</h1>".format(redis.get_pv())在 ~/docker/redis/ 目錄下,配置 redis.conf 文件cd ~/docker/redis# 獲取官方提供的redis配置文件模板wget http://download.redis.io/redis-stable/redis.conf# 修改redis密碼echo "requirepass 12345678" >> redis.conf配置文件修改好后,用它來配置 Docker 的 redis 容器應用。docker run --restart=always --network host -d -it -v ~/docker/redis/redis.conf:/usr/local/etc/redis/redis.conf --name myredis redis redis-server /usr/local/etc/redis/redis.confredis容器啟動完成后,在 ~/test/ 目錄下執行 python3 -m flask run,打開Linux系統中的瀏覽器,輸入127.0.0.1:5000 訪問這個 web 應用的主頁,多次刷新查看效果。

1. 網絡爬蟲之網站分析

這一節我們來爬取一個圖書網站:互動出版網。之所以選擇這個網站,主要是它的數據比較好爬取,沒有反爬蟲機制,且網站的結構也不復雜,比較適合作為菜鳥進行練手。我們首先來分析網站及其相關的 HTML 元素,確定要爬取的內容?;映霭婢W的網站首頁如下:互動出版網首頁可以看到,這個網站沒有用到 https,依舊使用的是 http 協議,這個網站是極不安全的。我們現在要爬取的是這個網站的計算機類的圖書,我們可以點擊全部圖書分類那里,得到所有圖書的分類情況?;映霭婢W全部圖書分類通過 F12 可以看到,每個計算機的分類對應著一個鏈接。我們點進去看就會得到對應分類下的圖書列表,還帶著分頁信息:互動出版網計算機分類圖書列表從這個頁面中,我們可以分析到很多,首先對于一個圖書信息,我們想要提取的數據有:圖書標題;圖書作者;出版社;ISBN;出版時間;圖書價格。至于圖書的詳情頁面我們就不再進去看了,詳情頁中能到到更多信息,比如總頁數、圖書簡介、目錄等等。此外,這里有一個分頁信息,通過多次點擊可以發現,只是前面的 url 中的一個數字發生了變化,因此我們可以直接構造出相應頁數的 url 請求,獲取其他頁的圖書列表、還等什么呢?開始激動人心的圖書數據爬取流程吧!?。?/p>

1.1 發出請求

注意 twisted.web.client.Agent 這個類,它是客戶端 API 的入口點,請求是使用 request() 方法發出的,該方法以請求方法、請求URI、請求頭和可以生成請求體的對象作為參數。代理負責連接設置。因此,它需要一個 reactor 作為初始值設定項的參數。來看官方給的第一個簡單例子:from __future__ import print_functionfrom twisted.internet import reactorfrom twisted.web.client import Agentfrom twisted.web.http_headers import Headersagent = Agent(reactor)d = agent.request( b'GET', b'http://www.xianlaiwan.cn/wiki/', Headers({'User-Agent': ['Twisted Web Client Example']}), None)def cbResponse(ignored): print('Response received')d.addCallback(cbResponse)# 關閉reactor()def cbShutdown(ignored): reactor.stop()d.addBoth(cbShutdown)reactor.run()上述代碼簡單實例化一個 agent,然后調用 request() 方法請求 http://www.xianlaiwan.cn/wiki/ 這個地址,這個動作也是一個延遲加載的方式;接下來的回調鏈中還會有請求完成后打印收到響應的方法以及最后關閉 reactor 的方法;執行的結果如下:[root@server2 scrapy-test]# python3 request.py Response received如果想要給請求帶上參數,就需要傳遞一個 twisted.web.iweb.IBodyProducer 類型的對象到 Agent.request。我們繼續來學習官方給出的第二個例子:下面的代碼給出了一個簡單的 IBodyProducer 實現,它向使用者寫入內存中的字符串# 代碼文件命名為:bytesprod.py from zope.interface import implementerfrom twisted.internet.defer import succeedfrom twisted.web.iweb import IBodyProducer@implementer(IBodyProducer)class BytesProducer(object): def __init__(self, body): self.body = body self.length = len(body) def startProducing(self, consumer): consumer.write(self.body) return succeed(None) def pauseProducing(self): pass def stopProducing(self): pass下面的代碼則在請求中帶上了 body 體:# 代碼文件:sendbody.pyfrom twisted.internet import reactorfrom twisted.web.client import Agentfrom twisted.web.http_headers import Headersfrom bytesprod import BytesProduceragent = Agent(reactor)# 構造請求體body = BytesProducer(b"hello, world")d = agent.request( b'POST', b'http://httpbin.org/post', Headers({'User-Agent': ['Twisted Web Client Example'], 'Content-Type': ['text/x-greeting']}), # 帶上body body)# 回調鏈,收到上個request的請求響應def cbResponse(ignored): print('Response received')d.addCallback(cbResponse)# 關閉reactordef cbShutdown(ignored): reactor.stop()d.addBoth(cbShutdown)reactor.run()

2. 如何深入插件源碼學習?

我們以 DRF 框架為例,聊一聊如何深入 DRF 框架的源碼學習。首先肯定是下載穩定版本為 DRF 源碼到本地,這是為了方便自己閱讀代碼。截止到2020年5月10日,DRF 的 Github 官方地址發布的最新版本為3.11.0,我們會用該版本的代碼來進行相關的演示和說明。以下是 DRF-3.11.0 源代碼截圖,里面的代碼量還是比較大的,不過相對于 Django 的代碼而言就會少很多,我們前面能學習并跟蹤 Django 框架的源碼,拿下 DRF 源碼自然也不在話下。一般而言,推薦學習一個 Django 第三方插件源碼的過程如下:第一步:熟練使用 Django 框架以及熟悉 Django 框架源碼。所有的 Django 第三方插件代碼里會大量調用 Django 源碼的類或者方法,并在其基礎上進行擴展或者進一步創新。以我們必須先掌握 Django 的源碼,才能繼續學習 DRF 的源碼;第二步:仔細閱讀官方文檔手冊進行學習,掌握框架的基本用法;第三步:通過官方文檔,實戰 DRF 框架;每次在用熟練 DRF 提供的類或者方法后,就可以對應地查看源碼,并分析 DRF 背后所做的工作。每掌握一個模塊的基本用法,就可以深入學習對應模塊的源碼,同時在源碼中我們還可以發現該模塊中的更多用法,然后再次實踐,以加深對源碼的理解。我們按照上面的過程來簡單走一遍。首先我們前面對 Django 的幾大模塊的源碼都有涉獵,算是滿足了第一步要求。接下來我們用官方給的快速入門教程完成我們的第一次 Django REST framework 框架的初體驗。模型序列化器:給會員表 member 添加一個序列化器類,放到新建的 serializers.py 文件中。from rest_framework import serializersfrom hello_app.models import Memberclass MemberSerializer(serializers.ModelSerializer): class Meta: model = Member fields = ("id", "name", "age", "sex", "occupation", "phone_num", "email", "city", "vip_level_id")準備 View 視圖:添加一個對會員表操作的視圖類,我們用最簡單的形式即可。# 代碼位置:hello_app/views.py# ...from rest_framework import viewsetsfrom rest_framework import permissions# ...class MemberViewSet(viewsets.ModelViewSet): # 設置queryset queryset = Member.objects.all().order_by('-register_date') # 設置序列化器 serializer_class = MemberSerializer # 設置認證器 permission_classes = [permissions.IsAuthenticated]編寫 URLConf 配置:Django REST framework 框架改良了 URLConf 配置的寫法,后面會研究這種寫法,先直接使用官方的示例即可。# 代碼位置:hello_app/urls.py# ...from rest_framework import routersrouter = routers.DefaultRouter()router.register(r'members', views.MemberViewSet)urlpatterns = [ # ... path('', include(router.urls))]另外,由于我們對 MemberViewSet 視圖加上了認證,所以必須要在入口的 urls.py 中上如下的 URLConf 的配置。# 代碼位置: first_django_app/urls.py# ...urlpatterns = [ # ... path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))]注意:不添加和添加這行 URLConf 配置的效果圖如下所示。接下來,最后一步是設置視圖的相關配置以及注冊 rest_framework 應用。# 代碼位置:first_django_app/settings.py# ...INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 注冊第三方應用 'rest_framework', # 注冊應用 'hello_app']REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 5}# ...最后我們啟動服務,來一起看看效果。我們之前創建過一個超級用戶admin/admin.1234!,接下來會用這個通過 DRF 的認證。25從上面的演示中,我們看到了 Django REST framework 框架給我們做的接口測試頁面,我們只需要簡單繼承下MemberViewSet 即可,然后添加相關屬性即可立即擁有這樣一個完整的接口測試頁面。后臺服務主要提供接口數據,我們也可以使用 curl 命令來獲取和操作相應的模型表。[root@server ~]# curl -H 'Accept: application/json; indent=4' -u admin:admin.1234! http://127.0.0.1:8888/hello/members/?page=3{ "count": 103, "next": "http://127.0.0.1:8888/hello/members/?page=4", "previous": "http://127.0.0.1:8888/hello/members/?page=2", "results": [ { "id": 9, "name": "spyinx-5", "age": "39", "sex": 0, "occupation": "product", "phone_num": "18015702646", "email": "[email protected]", "city": "shanghai", "vip_level_id": null }, { "id": 10, "name": "spyinx-6", "age": "26", "sex": 0, "occupation": "ops", "phone_num": "18790082215", "email": "[email protected]", "city": "beijing", "vip_level_id": null }, { "id": 11, "name": "spyinx-7", "age": "23", "sex": 0, "occupation": "security", "phone_num": "18354491889", "email": "[email protected]", "city": "guangzhou", "vip_level_id": null }, { "id": 12, "name": "spyinx-8", "age": "26", "sex": 1, "occupation": "ui", "phone_num": "18406891676", "email": "[email protected]", "city": "wuhan", "vip_level_id": null }, { "id": 13, "name": "spyinx-9", "age": "26", "sex": 0, "occupation": "ops", "phone_num": "18036496230", "email": "[email protected]", "city": "wuhan", "vip_level_id": null } ]}在上面這個過程走通之后,我們可以看到其實這個例子中已經涉及到了 DRF 中的許多類,比如用于序列化的類ModelSerializer、視圖類 ModelViewSet、分頁類 PageNumberPagination 等等。從這個案例中,我們可以找到許多學習 DRF 源碼的切入點。首先看用到的視圖類 ModelViewSet:# 源碼位置:rest_framework/viewsets.pyclass ModelViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, GenericViewSet): """ A viewset that provides default `create()`, `retrieve()`, `update()`, `partial_update()`, `destroy()` and `list()` actions. """ pass通過學習 Django 的視圖,我們了解了 Mixin 這個概念,所以容易理解這里的代碼,視圖繼承 GenericViewSet,同時也繼承了數個 Mixin。這些 Mixin 從命名上就很容易知道其功能用法。進一步翻看其實現類,也能發現其具體含義 。以 mixins.CreateModelMixin 類為例:# rest_framework/mixins.pyclass CreateModelMixin: """ Create a model instance. """ def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def perform_create(self, serializer): serializer.save() def get_success_headers(self, data): try: return {'Location': str(data[api_settings.URL_FIELD_NAME])} except (TypeError, KeyError): return {}CreateModelMixin 的主要功能就是提供了 create() 方法,讓視圖擁有新增記錄的功能。其他的 Mixin 會提供類似的函數,讓視圖具有某一特定的功能。接下來我們的重點放到 GenericViewSet 類的學習上。# 源碼位置:rest_framework/viewsets.pyclass GenericViewSet(ViewSetMixin, generics.GenericAPIView): """ The GenericViewSet class does not provide any actions by default, but does include the base set of generic view behavior, such as the `get_object` and `get_queryset` methods. """ pass這里又是多繼承,一個 ViewSetMixin 類,另一個 generics.GenericAPIView 類。先追后面的 View 類,實現代碼如下:從這里我們看到了一些熟悉的屬性,如 queryset,serializer_class 以及用于分頁的 pagination_class。這個繼承的 APIView 類同樣也是 Django REST framework 框架自己定義的類,我們繼續追進 APIView 類的實現代碼:最后 APIView 這個類繼承的 View 正是 Django 中我們學過的 View 視圖類。from django.views.generic import View那這樣子,我們也算清楚了一些事情。Django REST framework 框架中定義的視圖是在 Django 的 View 視圖類上封裝和改進來的?,F在一個疑問就來了,看我們前面使用 Django 的視圖中,URLConf 配置如下: urlpatterns = [ path('test-cbv/', views.TestView.as_view(), name="test-cbv"), ]我們也分析過對應的 View 類以及 as_view() 方法,它將 GET 請求映射到視圖類的 get() 方法,POST 請求則映射到 post() 方法;然而我們這里一路走下來并有沒有看到對應的 get() 或者 post() 方法。但是視圖類繼承的多個 Mixin 中提供了 create()、list() 等這樣的方法,那么他們是如何和 URLConf 配置對應上的呢?我們現在要通過代碼去找出前面配置 URLConf 代碼的內部原理:from django.conf.urls import includefrom rest_framework import routersrouter = routers.DefaultRouter()router.register(r'members', views.MemberViewSet)urlpatterns = [ # ... path('', include(router.urls))]來看看上面的 URLConf 配置。這個時候,我們需要去看 Django REST Framework 中的 DefaultRouter 類,包括注冊方法 register() 以及 urls 屬性值的獲取。最后還要看 Django 中的 include() 方法的代碼,才能理清楚 URL 和視圖的映射關系。先追蹤 Django REST Framework 中的 DefaultRouter 類實現,該類繼承自 SimpleRouter,SimpleRouter 又繼承自 BaseRouter。為了加快速度,我們直接定位到基類 BaseRouter,可以看到 register() 方法和 urls 屬性的定義,如下:# 源碼位置:rest_framework/routers.pyclass BaseRouter: def __init__(self): self.registry = [] def register(self, prefix, viewset, basename=None): if basename is None: basename = self.get_default_basename(viewset) self.registry.append((prefix, viewset, basename)) # invalidate the urls cache if hasattr(self, '_urls'): del self._urls def get_default_basename(self, viewset): """ If `basename` is not specified, attempt to automatically determine it from the viewset. """ raise NotImplementedError('get_default_basename must be overridden') def get_urls(self): """ Return a list of URL patterns, given the registered viewsets. """ raise NotImplementedError('get_urls must be overridden') @property def urls(self): if not hasattr(self, '_urls'): self._urls = self.get_urls() return self._urls可以看到,在執行 router.register(r'members', views.MemberViewSet) 后其實等同于給 registry 數組添加一個元組元素,用于存儲映射關系。而 urls 屬性值則是調用 get_urls() 方法得到的。class DefaultRouter(SimpleRouter): """ The default router extends the SimpleRouter, but also adds in a default API root view, and adds format suffix patterns to the URLs. """ # ... def get_urls(self): """ Generate the list of URL patterns, including a default root view for the API, and appending `.json` style format suffixes. """ urls = super().get_urls() if self.include_root_view: view = self.get_api_root_view(api_urls=urls) root_url = url(r'^$', view, name=self.root_view_name) urls.append(root_url) if self.include_format_suffixes: urls = format_suffix_patterns(urls) return urls可以看到它先是調用了父類的 get_urls() 方法,另外又添加了一些映射規則。我們添加如下一行 print() 語句:class DefaultRouter(SimpleRouter): def get_urls(self): """ Generate the list of URL patterns, including a default root view for the API, and appending `.json` style format suffixes. """ urls = super().get_urls() print('父類調用得到的urls={}'.format(urls)) # ...然后啟動服務,可以看到如下的結果:(django-manual) [root@server first_django_app]# python manage.py runserver 0:8888Watching for file changes with StatReloaderPerforming system checks...父類調用得到的urls=[<URLPattern '^members/$' [name='member-list']>, <URLPattern '^members/(?P<pk>[^/.]+)/$' [name='member-detail']>]System check identified no issues (0 silenced).May 15, 2020 - 13:30:04Django version 2.2.12, using settings 'first_django_app.settings'Starting development server at http://0:8888/Quit the server with CONTROL-C可以看到,這個 ^members/$ 的URL 配置是由父類的 get_urls() 方法得到的。在父類 SimpleRouter 中的get_urls()方法中,我已經做好了相關的注釋,最關鍵的代碼就在最后的 append() 部分,那里添加的便是最后 URL 和 視圖函數的關系。class SimpleRouter(BaseRouter): # ... def get_urls(self): # ... # 前面介紹過這個 registry 屬性,就是通過 register() 方法得到的 for prefix, viewset, basename in self.registry: # ... for route in routes: # Only actions which actually exist on the viewset will be bound mapping = self.get_method_map(viewset, route.mapping) if not mapping: continue # Build the url pattern regex = route.url.format( prefix=prefix, lookup=lookup, # 尾部加上"/" trailing_slash=self.trailing_slash ) # 處理一些簡單情況 if not prefix and regex[:2] == '^/': regex = '^' + regex[2:] initkwargs = route.initkwargs.copy() initkwargs.update({ 'basename': basename, 'detail': route.detail, }) # 最最核心的部分代碼,這里得到視圖函數 view = viewset.as_view(mapping, **initkwargs) # 視圖名稱 name = route.name.format(basename=basename) # 添加映射規則 ret.append(url(regex, view, name=name)) return ret 我們可以看到最后添加的映射規則就是這一句:ret.append(url(regex, view, name=name)) ,我們繼續看看這個 url() 方法,它調用的正是 Django 中的 url() 方法,內容如下:# 源碼路徑:django/conf/urls.py# ...def url(regex, view, kwargs=None, name=None): return re_path(regex, view, kwargs, name)這個 url() 方法和我們之前在 Django 中用 repath() 以及 path() 差不多一致的。第一個參數是 url 規則,第二個便是視圖函數。比較重要的就是這里得到 view 的函數了,它便是真正的視圖函數。它和前面 Django 中的一樣,通過 as_view() 得到的。那么這個 as_view() 方法在哪呢,通過父類追蹤,可知 Django 的父類中本身就有 as_view() 方法,但是在前一個繼承的Mixin 中重寫了該方法,因此調用的便是該 Mixin 中的 as_view() 方法:class ViewSetMixin: """ This is the magic. Overrides `.as_view()` so that it takes an `actions` keyword that performs the binding of HTTP methods to actions on the Resource. For example, to create a concrete view binding the 'GET' and 'POST' methods to the 'list' and 'create' actions... view = MyViewSet.as_view({'get': 'list', 'post': 'create'}) """ @classonlymethod def as_view(cls, actions=None, **initkwargs): """ Because of the way class based views create a closure around the instantiated view, we need to totally reimplement `.as_view`, and slightly modify the view function that is created and returned. """ # ... def view(request, *args, **kwargs): self = cls(**initkwargs) # We also store the mapping of request methods to actions, # so that we can later set the action attribute. # eg. `self.action = 'list'` on an incoming GET request. self.action_map = actions # Bind methods to actions # This is the bit that's different to a standard view for method, action in actions.items(): handler = getattr(self, action) setattr(self, method, handler) if hasattr(self, 'get') and not hasattr(self, 'head'): self.head = self.get self.request = request self.args = args self.kwargs = kwargs # And continue as usual return self.dispatch(request, *args, **kwargs) # take name and docstring from class update_wrapper(view, cls, updated=()) # and possible attributes set by decorators # like csrf_exempt from dispatch update_wrapper(view, cls.dispatch, assigned=()) # We need to set these on the view function, so that breadcrumb # generation can pick out these bits of information from a # resolved URL. view.cls = cls view.initkwargs = initkwargs view.actions = actions return csrf_exempt(view)和 Django 中的一樣,這里最后的 as_view() 方法最后返回的便是視圖函數。那么對應的 /hello/members/ 請求進來后,有 view() 方法進行處理,最后調用的和 Django 中的一樣:return self.dispatch(request, *args, **kwargs)我們去 Django 中看這個 dispatch() 方法的源碼:# 源碼位置:django/views/generic/base.pyclass View: """ Intentionally simple parent class for all views. Only implements dispatch-by-method and simple sanity checking. """ # ... def dispatch(self, request, *args, **kwargs): # Try to dispatch to the right method; if a method doesn't exist, # defer to the error handler. Also defer to the error handler if the # request method isn't on the approved list. if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler = self.http_method_not_allowed return handler(request, *args, **kwargs)那么執行 /hello/members/ 請求到這里是,handler 是哪個?我們繼續翻看前面的 Mixin 類,有這樣一段代碼:class ViewSetMixin: # ... @classonlymethod def as_view(cls, actions=None, **initkwargs): def view(request, *args, **kwargs): # ... for method, action in actions.items(): handler = getattr(self, action) setattr(self, method, handler) # ... # ... # ...這里就非常明顯了,我們大概也能猜到一些。就是設置 (get|post|put|delete) 請求對應的方法,比較好的方式時我們在這里打印下請求,并在前端進行下請求測試,看看這里到底設置了啥?class ViewSetMixin: # ... @classonlymethod def as_view(cls, actions=None, **initkwargs): def view(request, *args, **kwargs): # ... for method, action in actions.items(): print('請求處理view視圖函數:method={}, action={}'.format(method, action)) handler = getattr(self, action) setattr(self, method, handler) # ... # ... # ...我們啟動服務請求以下路徑 /hello/members/,可以得到如下輸出結果:(django-manual) [root@server first_django_app]# python manage.py runserver 0:8888Watching for file changes with StatReloaderPerforming system checks...System check identified no issues (0 silenced).May 15, 2020 - 17:07:38Django version 2.2.12, using settings 'first_django_app.settings'Starting development server at http://0:8888/Quit the server with CONTROL-C.請求處理view視圖函數:method=get, action=list請求處理view視圖函數:method=post, action=create[15/May/2020 17:07:43] "GET /hello/members/ HTTP/1.1" 200 14426結合 Django 中的 dispatch() 方法,我們終于知道了 get 請求最后會調用視圖類中的 list() 方法去處理,而這個 list() 方法正是 ListModelMixin 中的。另外 post 請求則對應著視圖類中的 create() 方法,而這個屬性則來自 CreateModelMixin。這樣我們總算理解了前面的 URLConf 的映射流程以及對應的真正視圖處理函數。帶著問題去追源碼是我比較推薦的一個學習方式。完成一個模塊的學習就要去思考,去追蹤這個案例背后的執行過程,這樣才能更好的掌握這個模塊。今天的分享就到此結束了,DRF 中還有很多代碼等著你們去探索,去實踐,祝大家學習愉快!

1.1 src 屬性

src 的全稱是 source,表示圖片的 URL 地址源。源就是表示圖片的地址路徑,這個路徑可以是相對路徑,也可以是絕對路徑。絕對路徑指的是一個包含網絡協議頭的完整路徑,常用的網絡協議是 HTTP 協議,例如 https://www.baidu.com/img/bd_logo1.png;相對路徑是指這個圖片文件跟當前的網頁在同一個服務域,例如:/img/bd_logo1.png。962有時當圖片過大時或者過多時,網頁加載可能會比較慢,這時需要針對圖片使用懶加載的方式(圖片懶加載),懶加載的原理就是將圖片的 src 先設置為空,網頁其他內容加載完之后,再通過 JavaScript 將 src 屬性賦值,例如:<div class="container"> <img src="img/loading.png" alt="1" data-src="photo-1.jpeg"> <img src="img/loading.png" alt="2" data-src="photo-2.jpeg"> <img src="img/loading.png" alt="3" data-src="photo-3.jpeg"></div><script>window.onload = function (){//頁面加載完之后再加載圖片 var a = document.getElementsByTagName("img");//獲取圖片DOM for(var i in a){ a[i].src = a[i].getAttribute("data-src") }}</script>點擊下面的“運行案例”,可以試試真實的運行效果:963以上代碼通過定義 onload 事件,將圖片延后加載。1.1.1 Base64 方式加載圖片我們都知道 exe、jpg、pdf 這些格式的文件是使用二進制方式保存的,其中包含很多無法顯示和打印的字符,如果要讓文本處理軟件能處理二進制數據,需要將其編碼成一種明文顯示的安全格式,Base64 是一種最常見的二進制編碼方法。有時為了方便處理,圖片并非使用二進制流方式保存,而是使用 Base64 方式編碼之后保存在數據庫,img 標簽的 src 屬性可以識別 Base64 格式的編碼圖片格式,例如:964

3.1 框架層說明

由于大部分的預先認證模式比較一致,Spring Security 為此提供了一個框架,用來實現預先認證的身份提供器。預先認證模塊的實現類均在包 org.springframework.security.web.authentication.preauth 之下。此處介紹幾個核心的成員對象。3.1.1 AbstractPreAuthenticatedProcessingFilter該類用來檢查當前安全上下文的有效性,如果內容為空,它會嘗試從 Http 請求中解析出用戶信息,并將其提交到認證管理器 AuthenticationManager。使用該基類需要實現如下兩個方法:獲取認證身份信息protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request);獲取認證憑證protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request);通過對這兩個方法的調用,該過濾器將創建一個票據對象實例 PreAuthenticatedAuthenticationToken ,該實例包含用戶認證結果,并用于認證過程。但是此處的認證過程,僅僅是用來獲取權限,只是為了滿足標準 Spring Security 流程而實施的步驟。和其他的安全過濾器類似,預先認證的過濾器包含一個 authenticationDetailsSource 屬性,用于保存預先認證流程中的相關數據。3.1.2 J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource如果前述過濾器配置了 authenticationDetailsSource,其權限信息可以通過 isUserInRole(String role) 方法判斷,角色信息的配置通過 MappableAttributesRetriever 獲取,或者通過 <security-role> 標簽配置到 web.xml 文件中。3.1.3 PreAuthenticatedAuthenticationProvider預先認證身份提供者中的 UserDetails 對象與其他的身份提供者邏輯略有不同,主要體現在獲取 Authentication 認證信息時,僅需用戶名參數即可。public interface AuthenticationUserDetailsService { UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException;}3.1.4 Http403ForbiddenEntryPoint對 AuthenticationEntryPoint 的配置用對未認證用戶來發起認證流程。在預先認證流程中,該配置不起作用,我們只需要配置 ExceptionTranslationFilter 用于處理安全過濾器無法獲得用戶信息的情形,通常在這種情況下,請求端將收到 403 錯誤。

3. Docker 安裝

在不同的操作系統中都可以安裝 Docker ,本節內容中只演示 Ubuntu 環境下的 Docker 安裝。本次安裝演示的 Ubuntu 版本為 Ubuntu 20.04.1 LTS 。apt 更換國內源在安裝應用之前,我們需要把 apt 更換為國內源,這里我們選擇阿里云的 mirros.aliyun.com。# 備份 apt 源列表文件sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak# 更換源為 mirros.aliyun.comsudo sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list# 更新源sudo apt-get cleansudo apt-get update更換完畢后,我們還需要安裝 apt 的一些工具,如 https,curl 等。安裝 apt 依賴包sudo apt-get -y install apt-transport-https ca-certificates curl gnupg-agent software-properties-common獲取 GPG 密鑰證書我們這里使用阿里云的鏡像來獲取 GPG 密鑰:curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -獲取成功會返回 OK ,我們使用 apt-key finger 命令查看:apt-key finger# 輸出密鑰信息/etc/apt/trusted.gpg--------------------pub rsa4096 2017-02-22 [SCEA]9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88uid [ unknown] Docker Release (CE deb) <[email protected]>sub rsa4096 2017-02-22 [S]密鑰添加成功后,我們就可以開始后續的安裝了。添加 Docker 源為了更快速的安裝 Docker,這里我們添加阿里云的 Docker 源,首先我們先使用 lsb_release -a 命令獲取當前系統的 Codename:lsb_release -a# 輸出系統信息No LSB modules are available.Distributor ID: UbuntuDescription: Ubuntu 20.04.1 LTSRelease: 20.04Codename: focal本系統的 Codename 也就是版本代碼為 focal,我們在添加 Docker 源的時候就會使用這個版本:# 添加 docker-ce 源,系統為 ubuntu,系統版本為 focal, stable 為 docker 穩定版。sudo add-apt-repository "deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu focal stable"執行完畢后,我們需要更新 apt 源:sudo apt-get updateDocker 安裝更新完源后,我們就可以開始安裝 Docker 組件了:sudo apt-get install docker-ce docker-ce-cli containerd.io執行這一行命令需要一點時間,稍等片刻。完成后我們就可以來查看 Docker 是否安裝成功了。查看 Docker 版本使用 docker -v 來查看 Docker 版本:docker -v# 輸出 docker 版本信息Docker version 19.03.13, build 4484c46d9d看到版本信息輸出就說明我們的 Docker 源安裝成功了。Tips: 如果安裝失敗,需要注意系統的版本和添加的 Docker 源是否能使用。安裝成功后,我們來添加 Docker Image 鏡像源。添加 Docker Image 鏡像源使用阿里云的 Docker Image 鏡像源,需要登錄阿里云官網開啟 容器鏡像服務:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors添加 Docker Image 鏡像源為阿里云鏡像,這里同學們使用自己賬號的加速器地址即可:sudo mkdir -p /etc/dockersudo tee /etc/docker/daemon.json <<-'EOF'{"registry-mirrors": ["https://xxxxxx.mirror.aliyuncs.com"]}EOF添加完畢后,我們就可以啟動我們的 Docker 服務了。啟動 Dockerservice docker start# 輸出啟動信息* Starting Docker: docker 啟動完成,接下來我們進行測試。Docker 測試執行測試命令:docker run hello-world輸出:Unable to find image 'hello-world:latest' locallylatest: Pulling from library/hello-world0e03bdcc26d7: Pull complete Digest: sha256:4cf9c47f86df71d48364001ede3a4fcd85ae80ce02ebad74156906caff5378bcStatus: Downloaded newer image for hello-world:latestHello from Docker!This message shows that your installation appears to be working correctly.To generate this message, Docker took the following steps:1. The Docker client contacted the Docker daemon.2. The Docker daemon pulled the "hello-world" image from the Docker Hub.(amd64)3. The Docker daemon created a new container from that image which runs theexecutable that produces the output you are currently reading.4. The Docker daemon streamed that output to the Docker client, which sent itto your terminal.To try something more ambitious, you can run an Ubuntu container with:$ docker run -it ubuntu bashShare images, automate workflows, and more with a free Docker ID:https://hub.docker.com/For more examples and ideas, visit:https://docs.docker.com/get-started/我們看到這段信息就說明,我們的 Docker 測試成功了。那么接下來,我們就可以使用 Docker 來安裝 Zookeeper 服務。

3.2 構建 OAuth2.0 客戶端

3.2.1 創建 Maven 項目創建 Maven 項目,本例中 Group ID 為 imooc.tonglei0429.springsecurity,Artifact ID 為 login.oauth2.github在控制臺輸入以下命令:mvn archetype:generate \ -DgroupId=imooc.tonglei0429.springsecurity \ -DartifactId=login.oauth2.github \ -DarchetypeArtifactId=maven-archetype-quickstart \ -DinteractiveMode=false等待執行完畢后,執行目錄下會自動生成名為 login.oauth2.github 的目錄,包含完整的 Maven 項目結構。3.2.2 添加相關依賴打開 login.oauth2.github/pom.xml 文件,在其中添加以下依賴項:<!-- Spring Security 框架的配置模塊 --><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>5.3.2.RELEASE</version></dependency><!-- Spring Security OAuth2 認證客戶端模塊 --><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-client</artifactId> <version>5.3.2.RELEASE</version></dependency><!-- Spring Security OAuth2 授權信息加密模塊 --><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> <version>5.3.2.RELEASE</version></dependency><!-- Spring Boot thymeleaf 啟動模塊,用于 Web 服務中的頁面模板 --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <version>2.3.0.RELEASE</version></dependency><!-- Spring Boot thymeleaf 啟動模塊,用于啟動 Web 服務 --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.3.0.RELEASE</version></dependency><!-- Spring Boot thymeleaf 啟動模塊,包含適用于 Thymeleaf 的 Spring Security 擴展 --><dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>3.0.4.RELEASE</version></dependency>添加完以上內容后,保持文件。在命令行中輸入 mvn compile 測試依賴是否添加成功。3.2.3 創建 Web 項目創建 src/main/resources 目錄,增加配置文件 application.yml,并添加如下配置:server: port: 8080spring: thymeleaf: cache: false修改 src/main/java/imooc/tonglei0429/springsecurity/App.java,使其作為 Spring Boot 項目啟動。@SpringBootApplicationpublic class App { public static void main( String[] args ) { SpringApplication.run(App.class, args); }}創建 src/main/resources/templates/index.html 頁,用于展示測試結果,index.html 文件內容如下:<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5"><head> <title>OAuth2 登錄測試</title> <meta charset="utf-8" /></head><body><div style="float: right" th:fragment="logout" sec:authorize="isAuthenticated()"> <div style="float:left"> <span style="font-weight:bold">User: </span><span sec:authentication="name"></span> </div> <div style="float:none"> </div> <div style="float:right"> <form action="#" th:action="@{/logout}" method="post"> <input type="submit" value="Logout" /> </form> </div></div><h1>Spring Security 示例:使用 OAuth2.0 + Github 實現用戶認證</h1><div> 您已成功登錄,當前用戶 ID 為: <span style="font-weight:bold" th:text="${userName}"></span> 所屬的客戶端名稱為: <span style="font-weight:bold" th:text="${clientName}"></span></div><div> </div><div> <span style="font-weight:bold">返回的用戶屬性:</span> <ul> <li th:each="userAttribute : ${userAttributes}"> <span style="font-weight:bold" th:text="${userAttribute.key}"></span>: <span th:text="${userAttribute.value}"></span> </li> </ul></div></body></html>為了使 http://localhost:8080/springsecurity/ 可以訪問,還需要配置該路徑的控制器,src/main/java/imooc/tonglei0429/springsecurity/web/OAuth2LoginController.java,其代碼如下:@Controllerpublic class OAuth2LoginController { @GetMapping("/") public String index(Model model, @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient, @AuthenticationPrincipal OAuth2User oauth2User) { model.addAttribute("userName", oauth2User.getName()); model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName()); model.addAttribute("userAttributes", oauth2User.getAttributes()); return "index"; }}此時如果運行項目,訪問 localhost:8080/springsecurity,系統會默認跳轉到 localhost:8080/login 要求登錄,我們需要繼續將認證源指向 Github。3.2.4 配置 OAuth2 認證服務器繼續配置 application.yml 文件,添加配置項 security.oauth2.client.registration.github.client-id 和 security.oauth2.client.registration.github.client-secret 兩項,其值來自于 Github 上注冊應用后的返回頁面。具體代碼如下:spring: thymeleaf: cache: false security: oauth2: client: registration: github: client-id: 300c4622cd932acad32a client-secret: 71cb7971c2260f84c7b59693fbcf97b9c118c638

2.2 Admin 中的 actions

我們前面看到,Admin 中的 actions 是控制模型列表的動作,比如頁面上批量更新操作等。來看如下的示例代碼:from django.contrib import adminfrom .models import Member, VIPLevel# Register your models here.def make_men(modeladmin, request, queryset): queryset.update(sex=0)make_men.short_description = "全部轉成男性"@admin.register(Member)class MemberAdmin(admin.ModelAdmin): actions_on_top = False actions_on_bottom = True date_hierarchy = 'register_date' empty_value_display = '-empty-' list_display = ('name', 'age', 'city', 'sex', 'occupation', 'phone_num') list_editable = ('age', 'sex') list_filter = ('occupation', 'city') ordering = ['-age'] list_per_page = 5 actions = [make_men]@admin.register(VIPLevel)class VIPLevelAdmin(admin.ModelAdmin): pass我們自定義了一個 make_men() 方法,這個方法會作為 MemberAdmin 對象的一個 action 方法。然后給該方法添加 short_description 屬性,最后將該方法添加到 ModelAdmin 對象的 actions 屬性中去。這樣在模型管理頁面的 Action 欄會出現對應的標簽,用于實現相應的動作,如下圖所示:該 action 方法固定有三個參數:當前的 ModelAdmin 對象實例;一個 HttpRequest 實例,用于表示當前的 HTTP 請求;用戶選中記錄對應組成的 QuerySet 實例;選擇我們定義的 action 方法,然后點擊 Go 按鈕,就可以將所有選中記錄的性別改為男性。操作結果如下:我們可以看到每個模型管理頁面的 Action 欄的下拉框都會有一個默認的動作,就是刪除選中的記錄。如果我們不想要這個動作選項,可以在 admin.py 中加上admin.site.disable_action 代碼即可。如果有的模型想要有這個選項,而另一個模型不想要這個選項,則可以使用如下的方式完成需求:# 關閉本應用模型的刪除選中動作選項admin.site.disable_action('delete_selected')# Member模型管理頁面不會有刪除選中動作選項@admin.register(Member)class MemberAdmin(admin.ModelAdmin): ...# VIPLevel模型管理頁面則會有刪除選中動作選項class VIPLevelAdmin(admin.ModelAdmin): actions = ['delete_selected', 'a_third_action'] ...最后,如果直接不想要這個 Action 欄,我們直接給模型對應的 ModelAdmin 對象設置 action = None 即可,結果如下:

2. Scrapy 數據處理流程

而下面這張圖是非常經典的 Scrapy 框架的數據處理流程圖,這張數據流圖非常重要,它詳細描述了 Scrapy 框架背后的運行流程。按照圖中的序號,我來給大家描述下 Scrapy 框架中的數據處理流程:Scrapy框架數據流圖Spider 構造 Request 請求并提交給 Scrapy 引擎;這步中存在中間件操作,可以對請求設置代理 IP 等后再發送給引擎;引擎將請求發生給調度器,調度器會根據請求中設置的優先級確定要執行的請求順序;引擎從調度器中獲取將要執行的請求;引擎通過下載中間件后,將請求傳給下載器執行網頁下載;得到 HTTP 響應結果并將其封裝成 Response 類,然后經過下載中間件處理后傳遞回引擎;引擎接收到請求的響應結果后,通過爬蟲的中間件將其發送給爬蟲 (Spider) 去處理,對應我們前面案例中的 parse() 方法或者自定義的回調方法 book_list_parse();爬蟲會將網頁數據抽取成一條數據實體 (Item) 以及新的請求(比如下一頁的 URL),再次發送給引擎處理;引擎將爬蟲獲取到的 Item 發送給項目管道 (Item Pipelines),在項目管道中我們可以實現數據的持久化,比如保存到 MySQL 數據庫或者 MongoDB 中 。同時,如果有新的請求也會發送給引擎,在繼續從第二步開始重復執行,直到最后調度器中沒有請求為止。大家將這八個步驟和我們前面實現的互動出版網數據爬取的代碼進行實例分析,是不是會有更進一步的理解呢?這上面的架構圖和數據流圖對于我們學習和理解 Scrapy 框架都非常重要,大家一定要掌握!

1. Scrapy 的 <code>settings.py</code> 配置

從前面的學習中我們知道,settings.py 是 Scrapy 使用 startproject 命令生成的,這里的配置會默認覆蓋 Scrapy 內置的配置項,這些默認的配置項都位于 Scrapy 的 scrapy/settings/default_settings.py 中:Scrapy的默認配置文件我們來看看 default_settings.py 中的一些默認配置項。AJAXCRAWL_ENABLED:通用爬取經常會抓取大量的 index 頁面;AjaxCrawlMiddleware 能幫助我們正確地爬取,AJAXCRAWL_ENABLED 配置正是開啟該中間件的開關。由于有些性能問題,且對于特定爬蟲沒有什么意義,該中間默認關閉;自動限速擴展 (AutoThrottle):這類配置主要是以 Scrapy 爬蟲以及正在抓取網站的負載來自動優化爬取速度。它能自動調整 Scrapy 達到最佳的爬取速度,使用者無需自己設置下載延遲,只要設置好最大并發請求數即可。來看看有關該擴展的配置項:AUTOTHROTTLE_ENABLED = False # 默認關閉 AUTOTHROTTLE_DEBUG = False # 關閉調試 AUTOTHROTTLE_MAX_DELAY = 60.0 # 最高下載延遲 AUTOTHROTTLE_START_DELAY = 5.0 # 初始化下載延遲 AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0 # Scrapy 同時請求目標網站的平均請求數下面四個配置用于設置爬蟲自動關閉條件:CLOSESPIDER_TIMEOUT:一個整數值,單位為秒。如果一個 spider 在指定的秒數后仍在運行, 它將以 closespider_timeout 的原因被自動關閉。 如果值設置為0 (或者沒有設置),spiders 不會因為超時而關閉;CLOSESPIDER_ITEMCOUNT:一個整數值,指定條目的個數。如果 spider 爬取條目數超過了設置的值, 并且這些條目通過 item pipelines 傳遞,spider 將會以 closespider_itemcount 的原因被自動關閉;CLOSESPIDER_PAGECOUNT:一個整數值,指定最大的抓取響應 (reponses) 數。 如果 spider 抓取數超過指定的值,則會以 closespider_pagecount 的原因自動關閉。 如果設置為0(或者未設置),spiders不會因為抓取的響應數而關閉;CLOSESPIDER_ERRORCOUNT:一個整數值,指定spider可以接受的最大錯誤數。 如果spider生成多于該數目的錯誤,它將以 closespider_errorcount 的原因關閉。 如果設置為0(或者未設置),spiders不會因為發生錯誤過多而關閉;以上四個參數在 default_settings.py 中設置的默認值都是0并發相關,的設置會較大影響 Scrapy 爬蟲的性能。下面是默認的配置值,其中都已經進行了詳細的注釋說明:# pipelines中并發處理items數CONCURRENT_ITEMS = 100# scrapy中并發下載請求數CONCURRENT_REQUESTS = 16# 對任何單個域執行的并發請求的最大數量CONCURRENT_REQUESTS_PER_DOMAIN = 8# 將對任何單個IP執行的并發請求的最大數量。如果非零CONCURRENT_REQUESTS_PER_IP = 0Cookie相關配置:# 是否啟用cookiesmiddleware。如果關閉,cookies將不會發送給web serverCOOKIES_ENABLED = True# 如果啟用,Scrapy將記錄所有在request(cookie 請求頭)發送的cookies及response接收到的cookies(set-cookie接收頭),這也會間接影響性能,因此默認關閉。COOKIES_DEBUG = False請求深度相關配置,比如 DEPTH_LIMIT 設置請求允許的最大深度。如果為 0 ,則表示不受限;DEPTH_STATS_VERBOSE 參數控制是否收集詳細的深度統計信息;如果啟用此選項,則在統計信息中收集每個深度的請求數。DEPTH_PRIORITY 參數用于根據深度調整請求優先級。來看看他們的默認設置:DEPTH_LIMIT = 0DEPTH_STATS_VERBOSE = FalseDEPTH_PRIORITY = 0DNS 相關配置。DNSCACHE_ENABLED 用于控制是否啟用 DNS 緩存,DNSCACHE_SIZE參數設置緩存大小,DNS_TIMEOUT 處理 DNS 查詢超時時間;我們來具體看看 default_settings.py 中的默認配置:DNSCACHE_ENABLED = TrueDNSCACHE_SIZE = 10000# 緩存解析器DNS_RESOLVER = 'scrapy.resolver.CachingThreadedResolver'DNS_TIMEOUT = 60下載器相關。這部分的配置比較多,也是主要影響性能的地方。我們對一些關鍵的配置進行說明,具體如下:DOWNLOAD_DELAY:下載器在從同一網站下載連續頁面之前應等待的時間,通過該配置可以限制爬蟲的爬取速度。此外,該設置也受RANDOMIZE_DOWNLOAD_DELAY 設置(默認情況下啟用)的影響。DOWNLOAD_TIMEOUT:下載超時時間;DOWNLOAD_MAXSIZE:下載器將下載的最大響應大??;DOWNLOAD_HANDLERS_BASE:處理不同類型下載的下載器;DOWNLOAD_FAIL_ON_DATALOSS:數據丟失后是否繼續下載;DOWNLOADER_MIDDLEWARES 和DOWNLOADER_MIDDLEWARES_BASE:分別表示自定義的下載中間件類和默認的下載中間件類;DOWNLOADER_STATS:是否啟用下載器統計信息收集。來看看 default_settings.py 中的默認配置,具體如下:DOWNLOAD_DELAY = 0DOWNLOAD_HANDLERS = {}DOWNLOAD_HANDLERS_BASE = { 'data': 'scrapy.core.downloader.handlers.datauri.DataURIDownloadHandler', 'file': 'scrapy.core.downloader.handlers.file.FileDownloadHandler', 'http': 'scrapy.core.downloader.handlers.http.HTTPDownloadHandler', 'https': 'scrapy.core.downloader.handlers.http.HTTPDownloadHandler', 's3': 'scrapy.core.downloader.handlers.s3.S3DownloadHandler', 'ftp': 'scrapy.core.downloader.handlers.ftp.FTPDownloadHandler',}DOWNLOAD_TIMEOUT = 180 # 3minsDOWNLOAD_MAXSIZE = 1024 * 1024 * 1024 # 1024mDOWNLOAD_WARNSIZE = 32 * 1024 * 1024 # 32mDOWNLOAD_FAIL_ON_DATALOSS = TrueDOWNLOADER = 'scrapy.core.downloader.Downloader'DOWNLOADER_HTTPCLIENTFACTORY = 'scrapy.core.downloader.webclient.ScrapyHTTPClientFactory'DOWNLOADER_CLIENTCONTEXTFACTORY = 'scrapy.core.downloader.contextfactory.ScrapyClientContextFactory'DOWNLOADER_CLIENT_TLS_CIPHERS = 'DEFAULT'# Use highest TLS/SSL protocol version supported by the platform, also allowing negotiation:DOWNLOADER_CLIENT_TLS_METHOD = 'TLS'DOWNLOADER_CLIENT_TLS_VERBOSE_LOGGING = FalseDOWNLOADER_MIDDLEWARES = {}DOWNLOADER_MIDDLEWARES_BASE = { # Engine side 'scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware': 100, 'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware': 300, 'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware': 350, 'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware': 400, 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': 500, 'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550, 'scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware': 560, 'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware': 580, 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 590, 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware': 600, 'scrapy.downloadermiddlewares.cookies.CookiesMiddleware': 700, 'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 750, 'scrapy.downloadermiddlewares.stats.DownloaderStats': 850, 'scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware': 900, # Downloader side}DOWNLOADER_STATS = TrueDUPEFILTER_CLASS:指定去重類;DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'自定義擴展和內置擴展配置:EXTENSIONS = {}EXTENSIONS_BASE = { 'scrapy.extensions.corestats.CoreStats': 0, 'scrapy.extensions.telnet.TelnetConsole': 0, 'scrapy.extensions.memusage.MemoryUsage': 0, 'scrapy.extensions.memdebug.MemoryDebugger': 0, 'scrapy.extensions.closespider.CloseSpider': 0, 'scrapy.extensions.feedexport.FeedExporter': 0, 'scrapy.extensions.logstats.LogStats': 0, 'scrapy.extensions.spiderstate.SpiderState': 0, 'scrapy.extensions.throttle.AutoThrottle': 0,}文件存儲相關:FILES_STORE_S3_ACL = 'private'FILES_STORE_GCS_ACL = ''FTP 服務配置, Scrapy 框架內置 FTP 下載程序。我們可以指定 FTP 的相關參數:FTP_USER = 'anonymous'FTP_PASSWORD = 'guest'FTP_PASSIVE_MODE = TrueHTTP 緩存相關配置。Scrapy 的 HttpCacheMiddleware 組件(默認情況下沒有啟用)提供了一個底層的對HTTP請求和響應的緩存。如果啟用的話(把HTTPCACHE_ENABLED設置為True),它會緩存每個請求和對應的響應。來看看和其相關的配置和含義:# 是否啟用http緩存HTTPCACHE_ENABLED = False# 緩存數據目錄HTTPCACHE_DIR = 'httpcache'HTTPCACHE_IGNORE_MISSING = False# 緩存存儲的插件HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'# 緩存過期時間HTTPCACHE_EXPIRATION_SECS = 0HTTPCACHE_ALWAYS_STORE = False# 緩存忽略的Http狀態碼HTTPCACHE_IGNORE_HTTP_CODES = []HTTPCACHE_IGNORE_SCHEMES = ['file']HTTPCACHE_IGNORE_RESPONSE_CACHE_CONTROLS = []HTTPCACHE_DBM_MODULE = 'dbm'# 設置緩存策略,DummyPolicy是所有請求均緩存,下次在請求直接訪問原來的緩存即可HTTPCACHE_POLICY = 'scrapy.extensions.httpcache.DummyPolicy'# 是否啟用緩存數據壓縮HTTPCACHE_GZIP = FalseItem 和 Item pipelines相關配置:# ITEM處理器ITEM_PROCESSOR = 'scrapy.pipelines.ItemPipelineManager'# 自定義的 item pipelinesITEM_PIPELINES = {}ITEM_PIPELINES_BASE = {}日志相關的配置:# 啟動日志功能LOG_ENABLED = True# 日志編碼LOG_ENCODING = 'utf-8'# 日志格式器LOG_FORMATTER = 'scrapy.logformatter.LogFormatter'# 日志格式LOG_FORMAT = '%(asctime)s [%(name)s] %(levelname)s: %(message)s'# 日志時間格式LOG_DATEFORMAT = '%Y-%m-%d %H:%M:%S'LOG_STDOUT = False# 日志級別LOG_LEVEL = 'DEBUG'# 指定日志輸出文件LOG_FILE = NoneLOG_SHORT_NAMES = False郵件配置:在 Scrapy 中提供了郵件功能,該功能使用十分簡便且采用了 Twisted 非阻塞模式,避免了對爬蟲的影響。我們只需要在 Scrapy 中進行簡單的設置,就能通過 API 發送郵件。郵件的默認配置項如下:MAIL_HOST = 'localhost'MAIL_PORT = 25MAIL_FROM = 'scrapy@localhost'MAIL_PASS = NoneMAIL_USER = None我們現在可以簡單的使用下 Scrapy 給我們提供的郵件類,來利用它給我們自己發送一封郵件。首先需要找下自己的 qq 郵箱或者其他郵箱,開啟 POP3/SMTP服務,然后我們可以得到一個授權碼。這個就是我們登陸這個郵箱服務的密碼。然后我們配置 settings.py 中的相應項:MAIL_HOST = 'smtp.qq.com'MAIL_PORT = 25MAIL_FROM = '[email protected]'MAIL_PASS = '你的授權碼'MAIL_USER = '[email protected]'接下來我們在 scrapy shell 中來調用相應的郵件接口,發送郵件:(scrapy-test) [root@server china_pub]# scrapy shell --nolog[s] Available Scrapy objects:[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)[s] crawler <scrapy.crawler.Crawler object at 0x7f1c3d4e9100>[s] item {}[s] settings <scrapy.settings.Settings object at 0x7f1c3d4e6dc0>[s] Useful shortcuts:[s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)[s] fetch(req) Fetch a scrapy.Request and update local objects [s] shelp() Shell help (print this help)[s] view(response) View response in a browser>>> from scrapy.mail import MailSender>>> mailer = MailSender().from_settings(settings)>>> mailer.send(to=["[email protected]"], subject="這是一個測試", body="來自百度云主機發送的一封郵件", cc=["[email protected]"])<Deferred at 0x7f1c3c4d1c40>調用 Scrapy 的郵件接口發送郵件內存相關參數:MEMDEBUG_ENABLED = False # enable memory debuggingMEMDEBUG_NOTIFY = [] # send memory debugging report by mail at engine shutdownMEMUSAGE_CHECK_INTERVAL_SECONDS = 60.0# 是否啟用內存使用擴展MEMUSAGE_ENABLED = True# 在關閉Scrapy之前允許的最大內存量,為0則不檢查MEMUSAGE_LIMIT_MB = 0# 要達到內存限制時通知的電子郵件列表MEMUSAGE_NOTIFY_MAIL = []# 在發送警告電子郵件通知之前,要允許的最大內存量(以兆字節為單位)。如果為零,則不會產生警告MEMUSAGE_WARNING_MB = 0調度器相關配置:# 調度器類SCHEDULER = 'scrapy.core.scheduler.Scheduler'# 指定調度器的三種隊列類SCHEDULER_DISK_QUEUE = 'scrapy.squeues.PickleLifoDiskQueue'SCHEDULER_MEMORY_QUEUE = 'scrapy.squeues.LifoMemoryQueue'SCHEDULER_PRIORITY_QUEUE = 'scrapy.pqueues.ScrapyPriorityQueue'# 正在處理響應數據的軟限制(以字節為單位),如果所有正在處理的響應的大小總和高于此值,Scrapy不會處理新的請求SCRAPER_SLOT_MAX_ACTIVE_SIZE = 5000000spider 中間件相關配置,有我們熟悉的 SPIDER_MIDDLEWARES 和 SPIDER_MIDDLEWARES_BASE,表示自定義的 Spider 中間件和 Scrapy 內置的 Spider 中間件;SPIDER_MIDDLEWARES = {}SPIDER_MIDDLEWARES_BASE = { # Engine side 'scrapy.spidermiddlewares.httperror.HttpErrorMiddleware': 50, 'scrapy.spidermiddlewares.offsite.OffsiteMiddleware': 500, 'scrapy.spidermiddlewares.referer.RefererMiddleware': 700, 'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware': 800, 'scrapy.spidermiddlewares.depth.DepthMiddleware': 900, # Spider side}指定模板文件目錄,這個在使用 scrapy startproject 項目名 命令創建項目時,對應的模板文件所在的目錄:TEMPLATES_DIR = abspath(join(dirname(__file__), '..', 'templates'))USER_AGENT:設置請求頭的 User-Agent 參數,用來模擬瀏覽器。我們通常都會添加一個瀏覽器的 User-Agent 值,防止爬蟲直接被屏蔽;Scrapy 的大體配置就是這些,還有一些沒有介紹到的參數,課后可以仔細查看官方文檔進行了解。

上傳文件

最后看一看 requests 庫中如何上傳文件:>>> url = 'https://httpbin.org/post'>>> files = {'file': open('/home/store/shen/start.sh', 'rb')}>>> r = requests.post(url, files=files)>>> r.text'{\n "args": {}, \n "data": "", \n "files": {\n "file": "#!/bin/bash\\n########################################################\\n# author: spyinx (https://blog.csdn.net/qq_40085317) #\\n# email: [email protected] #\\n# date: 2020/6/24 #\\n# function: start agent server on CentOS 7.7 #\\n########################################################\\nAGENT_PORT=8765\\n\\n# check the agent process first\\nmain_pid=$(pstree -ap|grep gunicorn|grep -v grep|awk \'NR==1{print}\'|grep -o \\"[0-9]*\\"|awk \'NR==1{print}\')\\nif [ -n \\"$main_pid\\" ]; then\\n echo \\"get the agent server\'s main pid: $main_pid\\"\\n sudo kill -9 $main_pid\\n echo \\"stop the server first\\"\\n sleep 15\\n process_num=$(ps -ef|grep gunicorn|grep -v grep|wc -l)\\n if [ $process_num -ne 0 ]; then\\n echo \\"close agent server failed\\uff0cexit!\\"\\n exit 1\\n fi\\nfi\\n\\n# start agent server\\nmaster_addr=$(cat /etc/hosts | grep `hostname` | awk \'{print $1}\')\\necho \\"start agent server\\"\\ngunicorn -w 4 -b $master_addr:$AGENT_PORT xstore_agent.agent:app --daemon\\nsleep 5\\nprocess_num=$(ps -ef|grep gunicorn|grep -v grep|wc -l)\\nif [ $process_num -eq 0 ]; then\\n echo \\"start agent server failed\\uff0cplease check it!\\"\\n exit 2\\nfi\\necho \\"start agent server success\\uff0cok!\\""\n }, \n "form": {}, \n "headers": {\n "Accept": "*/*", \n "Accept-Encoding": "gzip, deflate", \n "Content-Length": "1356", \n "Content-Type": "multipart/form-data; boundary=565e2040b1d37bad527477863e64ba6c", \n "Host": "httpbin.org", \n "User-Agent": "python-requests/2.24.0", \n "X-Amzn-Trace-Id": "Root=1-5ef49e5f-a02b3e64f58fe4a3ff51fa94"\n }, \n "json": null, \n "origin": "47.115.61.209", \n "url": "https://httpbin.org/post"\n}\n'>>>在 requests 庫中,只需要將上傳文件參數傳遞給 post() 方法即可,是不是非常簡單?另外,我們還可以在請求中添加 cookie 或者在相應中獲取相應的 cookie 信息。另外,我們還可以使用 requests 的 Session 來維持會話,這在有登錄需求的網站獲取數據時會非常有用:# 創建一個session對象,用來存儲session信息>>> s = requests.session() >>> s.get("http://www.baidu.com") 如果在登錄之后,繼續使用 session 對象再請求該網站的其他頁面的 url,就會帶著 session 信息去與該網站進行交互,模擬登錄后的訪問。

3. Scrapy 源碼初探

看過了 Scrapy 的架構和數據處理流程,我們來簡單了解下 Scrapy 框架的源碼結構。熟悉和理解 Scrapy 框架的源碼,對于我們日常開發的好處不言而喻,我總結了如下三點原因:熟悉掌握 Scrapy 框架用法的最好方式就是閱讀源碼;提升編程能力的最好途徑也是閱讀源碼;此外,Twisted 模塊在 Scrapy 框架中應用廣泛,而國內關于該框架資源十分匱乏,我們可以借助 Scrapy 框架來完整學習 Twisted 模塊的使用,體驗這樣一個異步通信機制帶給我們的性能體驗;方便問題排錯以及后續基于 Scrapy 的深度定制開發。只有熟悉了 Scrapy 源碼,我們才能針對 Scrapy 框架進行深度定制開發,實現與我們業務相關的爬蟲框架;另外,熟悉源碼能方便我們在調試 Scrapy 爬蟲時快速定位為題原因,高效解決問題,這是一個經驗豐富的爬蟲工程師必須具備的技能;截止到這篇文章撰寫完成(2020 年 7 月 12 日),Scrapy 最新發布的版本是 2.2.0 版本。我們從 github 上選擇穩定的 scrapy 源碼包,下載下來后解壓并使用 VScode 軟件打開該源碼目錄。下面就是 Scrapy-2.2.0 的源碼內容:Scrapy-2.2.0版本源碼結構我們依次對這些源碼目錄和文件進行簡單說明,后面我們在學習中會逐漸深入源碼去分析 Scrapy 框架的行為以及追蹤爬蟲的執行過程。來依次看看 Scrapy 源碼的目錄結構:commands目錄:該目錄下的文件正是 scrapy 所支持的命令。比如我們生成爬蟲項目使用的命令 scrapy startproject xxxx 對應著文件 startproject.py,命令 scrapy shell http://www.baidu.com 對應的執行文件為 shell.py。因此,如果我們要追蹤 scrapy 命令的運行過程就可以從這里入手跟蹤代碼了;contracts 目錄:定義了若干簡單 python 文件;core 目錄:非常核心,定義了 scrapy 框架中的核心類與方法;downloadermiddleswares 目錄:下載中間件相關的代碼;extensions 目錄:定義了一些擴展方法,比如 debug.py,telnet.py 等代碼文件;http 目錄:該目錄下定義了 Request 和 Response 類及其相關的擴展類。下節和下下節會詳細介紹該目錄下的源碼文件;linkextractors 目錄:這里的代碼主要是輔助 scrapy 核心對網頁的鏈接進行抽取,定義了一系列的抽取類;loader目錄:該目錄下的代碼是 Item Loader,具體信息可以參考源碼下的 docs/topics/loaders.rst 文件進行深入了解;pipelines 目錄:和 pipelines 模塊相關,主要對爬蟲抽取到的 items 數據進行再次處理;selector 目錄:該目錄下定義了解析網頁的 Selector,主要是封裝了 parsel 模塊的 Selector 和 SelectorList;settings 目錄:這里定義了操作配置文件的相關類以及一個默認的配置文件 (default_settings.py);spidermiddlewares 目錄:定義了爬蟲中間件的相關類與方法,spiders 目錄:定義了爬蟲模塊的核心類;templates 目錄:下面保存了創建 scrapy 項目所需要的一些模板文件;utils 目錄:一些常用的輔助函數;其他文件:一些比較核心的代碼文件都在這里,比如 cmdline.py、crawler.py、shell.py 等??赐赀@些介紹后有沒有什么感覺?是不是覺得一個非常流行的 Python 框架也就那樣?當然不是,在這里我們只是介紹了它的表面,并沒有深究其內部細節。任何一個流行的框架必有其獨特的優勢,且必須代碼精良。Scrapy 框架在簡潔易用上做的非常不錯,此外由于其使用 Twisted 作為其異步處理框架,因此基于 Scrapy 框架開發的爬蟲一般具有不錯的性能,加之良好的社區、文檔和框架生態,終造就了今天的 Scrapy。接下來,我們會慢慢深入這些目錄去探索 Scrapy 的源碼,最后實現徹底掌握 Scrapy 框架的目的。

6. 狀態碼(Status Codes)

服務器向用戶返回的狀態碼和提示信息,常見的有以下一些(方括號中是該狀態碼對應的 HTTP 動詞)。不同的狀態碼代表著不同的含義,比如以 2 開頭的狀態碼通常代表服務器成功響應,3 開頭的狀態碼代表發生了重定性(即跳轉到了別的鏈接),4 開頭的狀態碼通常表示客戶端這邊提供的信息有誤,而 5 開頭的狀態碼則表示服務器內部出現的錯誤。通過返回的狀態碼,用戶即可判斷請求成功與否,不成功問題在何處。一些常用的狀態碼列舉如下:200 OK - [GET]:服務器成功返回用戶請求的數據201 CREATED - [POST/PUT/PATCH]:用戶新建或修改數據成功。202 Accepted - [*]:表示一個請求已經進入后臺排隊(異步任務)204 NO CONTENT - [DELETE]:用戶刪除數據成功400 INVALID REQUEST - [POST/PUT/PATCH]:用戶發出的請求有錯誤,服務器沒有進行新建或修改數據的操作401 Unauthorized - [*]:表示用戶沒有權限(令牌、用戶名、密碼錯誤)403 Forbidden - [*] 表示用戶得到授權(與401錯誤相對),但是訪問是被禁止的404 NOT FOUND - [*]:用戶發出的請求針對的是不存在的記錄,服務器沒有進行操作,該操作是冪等的406 Not Acceptable - [GET]:用戶請求的格式不可得(比如用戶請求JSON格式,但是只有XML格式)410 Gone -[GET]:用戶請求的資源被永久刪除,且不會再得到的422 Unprocesable entity - [POST/PUT/PATCH]: 當創建一個對象時,發生一個驗證錯誤500 INTERNAL SERVER ERROR - [*]:服務器發生錯誤,用戶將無法判斷發出的請求是否成功狀態碼的完全列表參見這里或這里。

1. 課程簡介

本課程的核心內容可以分為三個部分,分別是需要理解記憶的計算機底層基礎,后端通用組件以及需要不斷編碼練習的數據結構和算法。計算機底層基礎可以包含計算機網絡、操作系統、編譯原理、計算機組成原理,后兩者在面試中出現的頻率很少,課程主要關注網絡和系統兩個模塊,計算機網絡模塊主要介紹了常見的 TCP 協議、HTTP/HTTPS 協議,操作系統模塊主要介紹了操作系統的進程和線程、內存管理的頁面置換算法等高頻題。后端通用組件主要分為存儲持久化數據的數據庫,存儲臨時數據的緩存以及通信中間件。常用的支持持久化存儲的數據庫有 Oracle Database、SQLite 以及 MySQL,其中 MysQL 已經是后端必備的數據庫技能。常用的緩存方案有 memcached、guava cache、Redis,其中 Redis 是目前大型應用系統首選的緩存組件,課程介紹了 Redis 的數據結構和應用問題。通信中間件則介紹了 RabbitMQ 的常見應用問題。數據結構和算法部分最能提現候選人的編程基本功以及邏輯思考能力,課程主要介紹了最常見的算法案例,例如快排、鏈表、二叉樹、動態規劃。課程總體上是面向工作 3 年內的初中級程序員以及準備面試后端崗位的應屆生,因為篇幅原因并不能涵蓋所有的面試題,候選人需要做到舉一反三,例如能夠通過對于二叉樹的各種遍歷操作總結得到解決二叉樹問題的遞歸算法模板,這也是本課程的最終目的。

2. 生成 Spring Boot 項目

打開 Spring Initializr 網址 http://start.spring.io ,根據我們項目的情況填入以下信息。Spring Initializr 生成 Spring Boot 項目這是第一次接觸 Spring Initializr ,我們來詳細了解界面上選項的作用。1. 構建方式選擇:此處我們選擇 Maven Project 即可,表示生成的項目使用 Maven 構建。當然我們也可以發現,Spring Boot 項目亦可采用 Gradle 構建,目前 Spring Boot 主流的構建方式還是 Maven; 2. 編程語言選擇:此處選擇 Java 即可; 3. Spring Boot 版本選擇: 2.x 版本與 1.x 版本還是有一些區別的,咱們學習肯定是選擇 2.x 新版本。此處雖然選擇了 2.2.6 版本,但是由于 2.2.6 版本剛推出沒多久,國內一些 Maven 倉庫尚不支持。后面我們手工改為 2.2.5 版本,便于使用國內 Maven 倉庫快速構建項目; 4. 所屬機構設置:Group 表示項目所屬的機構,就是開發項目的公司或組織。因為公司可能會重名,所以習慣上采用倒置的域名作為 Group 的值。例如慕課網的域名是 imooc.com , 此處寫 com.imooc 就行了;5. 項目標識設置:Artifact 是項目標識,用來區分項目。此處我們命名為 spring-boot-hello ,注意項目標識習慣性地采用小寫英文單詞,單詞間加橫杠的形式。比如 Spring Boot 官方提供的很多依賴,都是 spring-boot-starter-xxx 的形式;6. 項目名稱設置:Name 是項目名稱,保持與 Artifact 一致即可; 7. 默認包名設置:Package name 是默認包名,保持默認即可; 8. 打包方式選擇:此處選擇將項目打包為 Jar 文件; 9. 添加項目依賴:此處不必修改,我們直接在 pom.xml 中添加依賴更加方便。注意 pom.xml 就是 Maven 的配置文件,可以指定我們項目需要引入的依賴; 10. 生成項目:點擊 Generate 按鈕,即可按我們設置的信息生成 Spring Boot 項目了。

2.2 Cookie的特點

Cookie 有如下特點:不可跨域名性:這個是 Cookie 非常重要的一個特點,域名限制。表示 A 網站頒發的 Cookie 在用戶訪問 B 網站時并不會被提交到 B 網站上去,這些都是由 Cookie 規范確定。此外,W3C 標準還禁止了 JavaScript 讀寫任何不屬于自己網站的 Cookie;時間限制:Cookie 同樣有著自己的生命周期,其 maxAge 值決定著 Cookie 的有效期,單位為秒。如果maxAge 屬性為正,則表示該 Cookie 會在 maxAge 秒之后自動失效。瀏覽器會將 maxAge 為正數的 Cookie持久化,即寫到對應的 Cookie 文件中。無論客戶關閉了瀏覽器還是電腦,只要還在 maxAge 秒之前,登錄網站時該 Cookie 仍然有效;如果 maxAge 為負數,則表示該 Cookie 僅在本瀏覽器窗口以及本窗口打開的子窗口內有效,關閉窗口后該 Cookie 即失效。maxAge 為負數的 Cookie,為臨時性 Cookie,不會被持久化,不會被寫到 Cookie 文件中。Cookie 信息保存在瀏覽器內存中,因此關閉瀏覽器該 Cookie 就消失了。Cookie 默認的 maxAge 值為–1;而如果 maxAge 為0,則表示刪除該 Cookie。Cookie 機制沒有提供刪除 Cookie 的方法,因此通過設置該 Cookie 即時失效實現刪除 Cookie 的效果。失效的 Cookie 會被瀏覽器從 Cookie 文件或者內存中刪除;空間限制:Cookie 只能存儲4-10KB;數量限制:一般而言,每個域下最多不能超過50個 Cookie存儲數據類型限制:Cookie只能存儲字符串從上面這些特點,我們也能發現使用 Cookie 的一些缺點。使用 Cookie 最大的問題就是安全性,因為 Cookie 是保存在客戶端的,且每次發送 HTTP 請求都會將 Cookie 帶過去。這些都導致 Cookie 很容易泄露出去,如果一旦 Cookie 泄露,且 Cookie 中設置的時間較長,那么很可能攻擊者拿著這個 Cookie 就能冒充受害者的身份去做一些事情,最后留下的記錄都是受害者的。然而接下來的 Session 在某種程度上避免了這一類的問題。

2. 適配器

所謂適配器組件,其本質就是運用適配器設計模式,匹配不兼容的接口規范。如上圖,調用者只能識別接口 2 類型,但是 A 提供的是接口 1 類型。適配器可以把接口 1 轉換成接口 2。這樣使用者就能使用 A 提供的功能了。為什么要使用適配器組件?欲解答這個問題,則先要了解如果不使用適配器組件,則如何編寫用戶控制器。既然稱為用戶控制器,則是開發者根據需要在框架外部定義的一個組件,Spring MVC 不可能未卜先知它的存在。如果要讓 Spring MVC 識別這個控制器,有一種方案 :預先定義好接口,強制性要求開發者在設計控制器時遵循接口規范。比如說實現 Controller 接口編寫控制器。@Controllerpublic class HelloAction implements org.springframework.web.servlet.mvc.Controller {public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { return null;}}除此之外,Spriing MVC 提供有更靈活的用戶控制器設計方案,可使用 “ 普通 JAVA 類” 充當控制器,控制器中的方法也可由開發者隨性命名。此時,就需要適配器組件把這些不符合規范的控制器以統一的接口方式告訴給 Spring MVC 。Spring MVC 提供了 3 個默認適配器:org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\ org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\ org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter這些適配對象都實現了 HandlerAdapter 接口,此接口就有一個統一的內部調用方法。@NullableModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;簡要描述 3 個適配器的應用場景:SimpleControllerHandlerAdapter: 簡單的控制器處理器適配器,支持實現了 Controller 接口的控制器;HttpRequestHandlerAdapter: http 請求處理器適配器,要求編寫的控制器時實現 HttpRequestHandler 接口。此類控制器可以很方便的獲取請求包中的相關信息。但,真正使用的并不多;RequestMappingHandlerAdapter: 注解處理器適配器,適配使用了注解的用戶控制器。本課程中的就是使用了此適配器,此適配器的實現比前兩個都復雜。因為有適配器的存在,可以讓控制器的設計變得靈活。Tips: 這 3 類適配器都是 Spring MVC 默認提供的,可以不用顯示配置,除非有定制需求。

直播
查看課程詳情
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號