Python Requests庫使用指南

本文爲譯文,原文連接 python-requests-library-guide
本人博客: 編程禪師html

requests 庫是用來在Python中發出標準的HTTP請求。 它將請求背後的複雜性抽象成一個漂亮,簡單的API,以便你能夠專一於與服務交互和在應用程序中使用數據。python

在本文中,你將看到 requests 提供的一些有用的功能,以及如何針對你可能遇到的不一樣狀況來自定義和優化這些功能。 你還將學習如何有效的使用 requests,以及如何防止對外部服務的請求致使減慢應用程序的速度。git

在本教程中,你將學習如何:github

  • 使用常見的HTTP方法發送請求
  • 定製你的請求頭和數據,使用查詢字符串和消息體
  • 檢查你的請求和響應的數據
  • 發送帶身份驗證的請求
  • 配置你的請求來避免阻塞或減慢你的應用程序

雖然我試圖包含儘量多的信息來理解本文中包含的功能和示例,但閱讀此文須要對HTTP有基礎的瞭解。shell

如今讓咱們深刻了解如何在你的應用程序中使用請求!編程

開始使用 requests

讓咱們首先安裝 requests 庫。 爲此,請運行如下命令:json

pip install requests
複製代碼

若是你喜歡使用 Pipenv 管理Python包,你能夠運行下面的命令:api

pipenv install requests
複製代碼

一旦安裝了 requests ,你就能夠在應用程序中使用它。像這樣導入 requests :緩存

import requests
複製代碼

如今你已經都準備完成了,那麼是時候開始使用 requests 的旅程了。 你的第一個目標是學習如何發出GET請求。安全


GET 請求

HTTP方法(如GET和POST)決定當發出HTTP請求時嘗試執行的操做。 除了GET和POST以外,還有其餘一些經常使用的方法,你將在本教程的後面部分使用到。

最多見的HTTP方法之一是GET。 GET方法表示你正在嘗試從指定資源獲取或檢索數據。 要發送GET請求,請調用 requests.get()

你能夠經過下面方式來向GitHub的 Root REST API 發出GET請求:

>>> requests.get('https://api.github.com')
<Response [200]>
複製代碼

恭喜! 你發出了你的第一個請求。 接下來讓咱們更深刻地瞭解該請求的響應。


響應

Response 是檢查請求結果的強有力的對象。 讓咱們再次發出相同的請求,但此次將返回值存儲在一個變量中,以便你能夠仔細查看其屬性和方法:

>>> response = requests.get('https://api.github.com')
複製代碼

在此示例中,你捕獲了 get() 的返回值,該值是 Response 的實例,並將其存儲在名爲 response 的變量中。 你如今可使用 response 來查看有關GET請求結果的所有信息。

狀態碼

您能夠從 Response 獲取的第一部分信息是狀態碼。 狀態碼會展現你請求的狀態。

例如,200 OK 狀態表示你的請求成功,而 404 NOT FOUND 狀態表示找不到你要查找的資源。 還有許多其它的狀態碼 ,能夠爲你提供關於你的請求所發生的具體狀況。

經過訪問 .status_code,你能夠看到服務器返回的狀態碼:

>>> response.status_code
200
複製代碼

.status_code 返回 200 意味着你的請求是成功的,而且服務器返回你要請求的數據。

有時,你可能想要在代碼中使用這些信息來作判斷:

if response.status_code == 200:
    print('Success!')
elif response.status_code == 404:
    print('Not Found.')
複製代碼

按照這個邏輯,若是服務器返回 200 狀態碼,你的程序將打印 Success! 若是結果是 404 ,你的程序將打印 Not Found.

requests 更進一步爲你簡化了此過程。 若是在條件表達式中使用 Response 實例,則在狀態碼介於 200400 之間時將被計算爲爲 True ,不然爲 False

所以,你能夠經過重寫 if 語句來簡化上一個示例:

if response:
    print('Success!')
else:
    print('An error has occurred.')
複製代碼

技術細節 : 由於 __ bool __()Response 上的重載方法 ,所以真值測試才成立。

這意味着從新定義了 Response 的默認行爲,用來在肯定對象的真值時考慮狀態碼。

請記住,此方法 不會驗證 狀態碼是否等於 200 。緣由是 200400 範圍內的其餘狀態代碼,例如 204 NO CONTENT304 NOT MODIFIED ,就意義而言也被認爲是成功的響應。

例如,204 告訴你響應是成功的,可是下消息體中沒有返回任何內容。

所以,一般若是你想知道請求是否成功時,請確保使用這方便的簡寫,而後在必要時根據狀態碼適當地處理響應。

假設你不想在 if 語句中檢查響應的狀態碼。 相反,若是請求不成功,你但願拋出一個異常。 你可使用 .raise_for_status()執行此操做:

import requests
from requests.exceptions import HTTPError

for url in ['https://api.github.com', 'https://api.github.com/invalid']:
    try:
        response = requests.get(url)

        # If the response was successful, no Exception will be raised
        response.raise_for_status()
    except HTTPError as http_err:
        print(f'HTTP error occurred: {http_err}')  # Python 3.6
    except Exception as err:
        print(f'Other error occurred: {err}')  # Python 3.6
    else:
        print('Success!')
複製代碼

若是你調用 .raise_for_status(),將針對某些狀態碼引起 HTTPError 異常。 若是狀態碼指示請求成功,則程序將繼續進行而不會引起該異常。

進一步閱讀:若是你不熟悉Python 3.6的 f-strings,我建議你使用它們,由於它們是簡化格式化字符串的好方法。

如今,你對於如何處理從服務器返回的響應的狀態碼瞭解了許多。 可是,當你發出GET請求時,你不多隻關心響應的狀態碼。 一般,你但願看到更多。 接下來,你將看到如何查看服務器在響應正文中返回的實際數據。

響應內容

GET 請求的響應一般在消息體中具備一些有價值的信息,稱爲有效負載。 使用 Response 的屬性和方法,你能夠以各類不一樣的格式查看有效負載。

要以 字節 格式查看響應的內容,你可使用 .content

>>> response = requests.get('https://api.github.com')
>>> response.content
b'{"current_user_url":"https://api.github.com/user","current_user_authorizations_html_url":"https://github.com/settings/connections/applications{/client_id}","authorizations_url":"https://api.github.com/authorizations","code_search_url":"https://api.github.com/search/code?q={query}{&page,per_page,sort,order}","commit_search_url":"https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}","emails_url":"https://api.github.com/user/emails","emojis_url":"https://api.github.com/emojis","events_url":"https://api.github.com/events","feeds_url":"https://api.github.com/feeds","followers_url":"https://api.github.com/user/followers","following_url":"https://api.github.com/user/following{/target}","gists_url":"https://api.github.com/gists{/gist_id}","hub_url":"https://api.github.com/hub","issue_search_url":"https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}","issues_url":"https://api.github.com/issues","keys_url":"https://api.github.com/user/keys","notifications_url":"https://api.github.com/notifications","organization_repositories_url":"https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}","organization_url":"https://api.github.com/orgs/{org}","public_gists_url":"https://api.github.com/gists/public","rate_limit_url":"https://api.github.com/rate_limit","repository_url":"https://api.github.com/repos/{owner}/{repo}","repository_search_url":"https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}","current_user_repositories_url":"https://api.github.com/user/repos{?type,page,per_page,sort}","starred_url":"https://api.github.com/user/starred{/owner}{/repo}","starred_gists_url":"https://api.github.com/gists/starred","team_url":"https://api.github.com/teams","user_url":"https://api.github.com/users/{user}","user_organizations_url":"https://api.github.com/user/orgs","user_repositories_url":"https://api.github.com/users/{user}/repos{?type,page,per_page,sort}","user_search_url":"https://api.github.com/search/users?q={query}{&page,per_page,sort,order}"}'

複製代碼

雖然 .content 容許你訪問響應有效負載的原始字節,但你一般但願使用 UTF-8 等字符編碼將它們轉換爲字符串。 當你訪問 .text 時,response 將爲你執行此操做:

>>> response.text
'{"current_user_url":"https://api.github.com/user","current_user_authorizations_html_url":"https://github.com/settings/connections/applications{/client_id}","authorizations_url":"https://api.github.com/authorizations","code_search_url":"https://api.github.com/search/code?q={query}{&page,per_page,sort,order}"...}"}'

複製代碼

由於對 bytes 解碼到 str 須要一個編碼格式,因此若是你沒有指定,請求將嘗試根據響應頭來猜想編碼格式。 你也能夠在訪問 .text 以前經過 .encoding 來顯式設置編碼:

>>> response.encoding = 'utf-8' # Optional: requests infers this internally
>>> response.text
'{"current_user_url":"https://api.github.com/user","current_user_authorizations_html_url":"https://github.com/settings/connections/applications{/client_id}","authorizations_url":"https://api.github.com/authorizations","code_search_url":"https://api.github.com/search/code?q={query}{&page,per_page,sort,order}"...}"}'
複製代碼

若是你看看響應,你會發現它其實是序列化的 JSON 內容。 要獲取字典內容,你可使用 .text 獲取 str 並使用json.loads() 對其進行反序列化。 可是,完成此任務的更簡單方法是使用 .json()

>>> response.json()
{'current_user_url': 'https://api.github.com/user', 'current_user_authorizations_html_url': 'https://github.com/settings/connections/applications{/client_id}'...}'}
複製代碼

.json() 返回值的類型是字典類型,所以你可使用鍵值對的方式訪問對象中的值。

你可使用狀態碼和消息體作許多事情。 可是,若是你須要更多信息,例若有關 response 自己的元數據,則須要查看響應頭部。

響應頭部

響應頭部能夠爲你提供有用的信息,例如響應有效負載的內容類型以及緩存響應的時間限制。 要查看這些頭部,請訪問 .headers

>>> response.headers
{'Server': 'GitHub.com', 'Date': 'Mon, 10 Dec 2018 17:49:54 GMT', 'Content-Type': 'application/json; charset=utf-8',...}
複製代碼

.headers 返回相似字典的對象,容許你使用鍵來獲取頭部中的值。 例如,要查看響應有效負載的內容類型,你能夠訪問 Content-Type

>>> response.headers['Content-Type']
'application/json; charset=utf-8'
複製代碼

可是,這個相似於字典的頭部對象有一些特別之處。 HTTP規範定義頭部不區分大小寫,這意味着咱們能夠訪問這些頭信息而沒必要擔憂它們的大小寫:

>>> response.headers['content-type']
'application/json; charset=utf-8'
複製代碼

不管您使用 'content-type' 仍是 'Content-Type',你都將得到相同的值。

如今,你已經學習了有關 Response 的基礎知識。 你已經看到了它最有用的屬性和方法。 讓咱們退後一步,看看自定義 GET 請求時你的響應如何變化。


查詢字符串參數

自定義 GET 請求的一種經常使用方法是經過URL中的 查詢字符串 參數傳遞值。 要使用 get() 執行此操做,請將數據傳遞給 params 。 例如,你可使用GitHub的Search API來查找 requests 庫:

import requests

# Search GitHub's repositories for requests
response = requests.get(
    'https://api.github.com/search/repositories',
    params={'q': 'requests+language:python'},
)

# Inspect some attributes of the `requests` repository
json_response = response.json()
repository = json_response['items'][0]
print(f'Repository name: {repository["name"]}')  # Python 3.6+
print(f'Repository description: {repository["description"]}')  # Python 3.6+
複製代碼

經過將字典 {'q':'requests + language:python'} 傳遞給 .get()params 參數,你能夠修改從Search API返回的結果。

你能夠像你剛纔那樣以字典的形式或以元組列表形式將 params 傳遞給 get():

>>> requests.get(
...     'https://api.github.com/search/repositories',
...     params=[('q', 'requests+language:python')],
... )
<Response [200]>
複製代碼

你甚至能夠傳 bytes 做爲值:

>>> requests.get(
...     'https://api.github.com/search/repositories',
...     params=b'q=requests+language:python',
... )
<Response [200]>
複製代碼

查詢字符串對於參數化GET請求頗有用。 你還能夠經過添加或修改發送的請求的頭部來自定義你的請求。


請求頭

要自定義請求頭,你可使用 headers 參數將HTTP頭部組成的字典傳遞給 get()。 例如,你能夠經過 Accept 中指定文本匹配媒體類型來更改之前的搜索請求,以在結果中突出顯示匹配的搜索字詞:

import requests

response = requests.get(
    'https://api.github.com/search/repositories',
    params={'q': 'requests+language:python'},
    headers={'Accept': 'application/vnd.github.v3.text-match+json'},
)

# View the new `text-matches` array which provides information
# about your search term within the results
json_response = response.json()
repository = json_response['items'][0]
print(f'Text matches: {repository["text_matches"]}')
複製代碼

Accept 告訴服務器你的應用程序能夠處理哪些內容類型。 因爲你但願突出顯示匹配的搜索詞,因此使用的是 application / vnd.github.v3.text-match + json,這是一個專有的GitHub的 Accept 標頭,其內容爲特殊的JSON格式。

在你瞭解更多自定義請求的方法以前,讓咱們經過探索其餘HTTP方法來拓寬視野。


其餘HTTP方法

除了 GET 以外,其餘流行的HTTP方法包括 POST ,``PUTDELETEHEADPATCHOPTIONSrequests爲每一個HTTP方法提供了一個方法,與get()具備相似的結構:

>>> requests.post('https://httpbin.org/post', data={'key':'value'})
>>> requests.put('https://httpbin.org/put', data={'key':'value'})
>>> requests.delete('https://httpbin.org/delete')
>>> requests.head('https://httpbin.org/get')
>>> requests.patch('https://httpbin.org/patch', data={'key':'value'})
>>> requests.options('https://httpbin.org/get')
複製代碼

調用每一個函數使用相應的HTTP方法向httpbin服務發出請求。 對於每種方法,你能夠像之前同樣查看其響應:

>>> response = requests.head('https://httpbin.org/get')
>>> response.headers['Content-Type']
'application/json'

>>> response = requests.delete('https://httpbin.org/delete')
>>> json_response = response.json()
>>> json_response['args']
{}
複製代碼

每種方法的響應中都會返回頭部,響應正文,狀態碼等。 接下來,你將進一步瞭解 POST,``PUTPATCH` 方法,並瞭解它們與其餘請求類型的區別。


消息體

根據HTTP規範,POSTPUT和不太常見的PATCH請求經過消息體而不是經過查詢字符串參數傳遞它們的數據。 使用 requests,你將有效負載傳遞給相應函數的 data 參數。

data 接收字典,元組列表,字節或類文件對象。 你須要將在請求正文中發送的數據調整爲與你交互的服務的特定格式。

例如,若是你的請求的內容類型是 application / x-www-form-urlencoded ,則能夠將表單數據做爲字典發送:

>>> requests.post('https://httpbin.org/post', data={'key':'value'})
<Response [200]>
複製代碼

你還能夠將相同的數據做爲元組列表發送:

>>> requests.post('https://httpbin.org/post', data=[('key', 'value')])
<Response [200]>
複製代碼

可是,若是須要發送JSON數據,則可使用 json 參數。 當你經過 json 傳遞JSON數據時,requests 將序列化你的數據併爲你添加正確的 Content-Type 標頭。

httpbin.orgrequests 做者 Kenneth Reitz 建立的一個很好的資源。 它是一種接收測試請求並響應有關請求數據的服務。 例如,你可使用它來檢查基本的POST請求:

>>> response = requests.post('https://httpbin.org/post', json={'key':'value'})
>>> json_response = response.json()
>>> json_response['data']
'{"key": "value"}'
>>> json_response['headers']['Content-Type']
'application/json'
複製代碼

你能夠從響應中看到服務器在你發送請求時收到了請求數據和標頭。 requests 還以 PreparedRequest 的形式向你提供此信息。


檢查你的請求

當你發出請求時,requests 庫會在將請求實際發送到目標服務器以前準備該請求。 請求準備包括像驗證頭信息和序列化JSON內容等。

你能夠經過訪問 .request 來查看 PreparedRequest:

>>> response = requests.post('https://httpbin.org/post', json={'key':'value'})
>>> response.request.headers['Content-Type']
'application/json'
>>> response.request.url
'https://httpbin.org/post'
>>> response.request.body
b'{"key": "value"}'
複製代碼

經過檢查 PreparedRequest ,你能夠訪問有關正在進行的請求的各類信息,例若有效負載,URL,頭信息,身份驗證等。

到目前爲止,你已經發送了許多不一樣類型的請求,但它們都有一個共同點:它們是對公共API的未經身份驗證的請求。 你遇到的許多服務可能都但願你以某種方式進行身份驗證。


身份驗證

身份驗證可幫助服務瞭解你的身份。 一般,你經過將數據傳遞到 Authorization 頭信息或服務定義的自定義頭信息來向服務器提供憑據。 你在此處看到的全部請求函數都提供了一個名爲 auth 的參數,容許你傳遞憑據。

須要身份驗證的一個示例API的是GitHub的 Authenticated User API。 此端點提供有關通過身份驗證的用戶配置文件的信息。 要向 Authenticated User API 發出請求,你能夠將你的GitHub的用戶名和密碼以元組傳遞給 get()

>>> from getpass import getpass
>>> requests.get('https://api.github.com/user', auth=('username', getpass()))
<Response [200]>
複製代碼

若是你在元組中傳遞給 auth 的憑據有效,則請求成功。 若是你嘗試在沒有憑據的狀況下發出此請求,你將看到狀態代碼爲 401 Unauthorized :

>>> requests.get('https://api.github.com/user')
<Response [401]>
複製代碼

當你以元組形式吧用戶名和密碼傳遞給 auth 參數時,rqeuests 將使用HTTP的基本訪問認證方案來應用憑據。

所以,你能夠經過使用 HTTPBasicAuth 傳遞顯式的基自己份驗證憑據來發出相同的請求:

>>> from requests.auth import HTTPBasicAuth
>>> from getpass import getpass
>>> requests.get(
...     'https://api.github.com/user',
...     auth=HTTPBasicAuth('username', getpass())
... )
<Response [200]>
複製代碼

雖然你不須要明確進行基自己份驗證,但你可能但願使用其餘方法進行身份驗證。 requests 提供了開箱即用的其餘身份驗證方法,例如 HTTPDigestAuthHTTPProxyAuth

你甚至能夠提供本身的身份驗證機制。 爲此,你必須首先建立AuthBase的子類。 而後,實現__call __()

import requests
from requests.auth import AuthBase

class TokenAuth(AuthBase):
    """Implements a custom authentication scheme."""

    def __init__(self, token):
        self.token = token

    def __call__(self, r):
        """Attach an API token to a custom auth header."""
        r.headers['X-TokenAuth'] = f'{self.token}'  # Python 3.6+
        return r


requests.get('https://httpbin.org/get', auth=TokenAuth('12345abcde-token'))
複製代碼

在這裏,你自定義的 TokenAuth 接收一個令牌,而後在你的請求頭中的 X-TokenAuth 頭中包含該令牌。

錯誤的身份驗證機制可能會致使安全漏洞,所以,除非服務因某種緣由須要自定義身份驗證機制,不然你始終但願使用像 BasicOAuth 這樣通過驗證的身份驗證方案。

在考慮安全性時,讓咱們考慮使用 requests 處理SSL證書。


SSL證書驗證

每當你嘗試發送或接收的數據都很敏感時,安全性就很重要。 經過HTTP與站點安全通訊的方式是使用SSL創建加密鏈接,這意味着驗證目標服務器的SSL證書相當重要。

好消息是 requests 默認爲你執行此操做。 可是,在某些狀況下,你可能但願更改此行爲。

若是要禁用SSL證書驗證,請將 False 傳遞給請求函數的 verify 參數:

>>> requests.get('https://api.github.com', verify=False)
InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
  InsecureRequestWarning)
<Response [200]>
複製代碼

當你提出不安全的請求時,requests 甚至會發出警告來幫助你保護數據安全。


性能

在使用 requests 時,尤爲是在生產應用程序環境中,考慮性能影響很是重要。 超時控制,會話和重試限制等功能能夠幫助你保持應用程序平穩運行。

超時控制

當你向外部服務發出請求時,系統將須要等待響應才能繼續。 若是你的應用程序等待響應的時間太長,則可能會阻塞對你的服務的請求,你的用戶體驗可能會受到影響,或者你的後臺做業可能會掛起。

默認狀況下,requests 將無限期地等待響應,所以你幾乎應始終指定超時時間以防止這些事情發生。 要設置請求的超時,請使用 timeout 參數。 timeout 能夠是一個整數或浮點數,表示在超時以前等待響應的秒數:

>>> requests.get('https://api.github.com', timeout=1)
<Response [200]>
>>> requests.get('https://api.github.com', timeout=3.05)
<Response [200]>
複製代碼

在第一個請求中,請求將在1秒後超時。 在第二個請求中,請求將在3.05秒後超時。

你還能夠將元組傳遞給 timeout,第一個元素是鏈接超時(它容許客戶端與服務器創建鏈接的時間),第二個元素是讀取超時(一旦你的客戶已創建鏈接而等待響應的時間):

>>> requests.get('https://api.github.com', timeout=(2, 5))
<Response [200]>
複製代碼

若是請求在2秒內創建鏈接並在創建鏈接的5秒內接收數據,則響應將按原樣返回。 若是請求超時,則該函數將拋出 Timeout 異常:

import requests
from requests.exceptions import Timeout

try:
    response = requests.get('https://api.github.com', timeout=1)
except Timeout:
    print('The request timed out')
else:
    print('The request did not time out')
複製代碼

你的程序能夠捕獲 Timeout 異常並作出相應的響應。

Session對象

到目前爲止,你一直在處理高級請求API,例如 get()post()。 這些函數是你發出請求時所發生的事情的抽象。 爲了你沒必要擔憂它們,它們隱藏了實現細節,例如如何管理鏈接。

在這些抽象之下是一個名爲 Session 的類。 若是你須要微調對請求的控制方式或提升請求的性能,則可能須要直接使用 Session 實例。

Session 用於跨請求保留參數。 例如,若是要跨多個請求使用相同的身份驗證,則可使用session

import requests
from getpass import getpass

# By using a context manager, you can ensure the resources used by
# the session will be released after use
with requests.Session() as session:
    session.auth = ('username', getpass())

    # Instead of requests.get(), you'll use session.get()
    response = session.get('https://api.github.com/user')

# You can inspect the response just like you did before
print(response.headers)
print(response.json())
複製代碼

每次使用 session 發出請求時,一旦使用身份驗證憑據初始化,憑據將被保留。

session 的主要性能優化以持久鏈接的形式出現。 當你的應用程序使用 Session 創建與服務器的鏈接時,它會在鏈接池中保持該鏈接。 當你的應用程序想要再次鏈接到同一服務器時,它將重用池中的鏈接而不是創建新鏈接。

最大重試

請求失敗時,你可能但願應用程序重試相同的請求。 可是,默認狀況下,requests 不會爲你執行此操做。 要應用此功能,您須要實現自定義 Transport Adapter

經過 Transport Adapters,你能夠爲每一個與之交互的服務定義一組配置。 例如,假設你但願全部對於https://api.github.com的請求在最終拋出 ConnectionError 以前重試三次。 你將構建一個 Transport Adapter,設置其 max_retries 參數,並將其裝載到現有的 Session

import requests
from requests.adapters import HTTPAdapter
from requests.exceptions import ConnectionError

github_adapter = HTTPAdapter(max_retries=3)

session = requests.Session()

# Use `github_adapter` for all requests to endpoints that start with this URL
session.mount('https://api.github.com', github_adapter)

try:
    session.get('https://api.github.com')
except ConnectionError as ce:
    print(ce)
複製代碼

當您將 HTTPAdapter(github_adapter)掛載到 session 時,session將遵循其對https://api.github.com的每一個請求的配置。

TimeoutsTransport AdaptersSessions 用於保持代碼高效和應用程序的魯棒性。


總結

在學習Python中強大的 requests 庫方面,你已經走了很長的路。

你如今可以:

  • 使用各類不一樣的HTTP方法發出請求,例如GET,POST和PUT
  • 經過修改請求頭,身份驗證,查詢字符串和消息體來自定義你的請求
  • 檢查發送到服務器的數據以及服務器發回給你的數據
  • 使用SSL證書驗證
  • 高效的使用requests 經過使用 max_retriestimeoutSessionsTransport Adapters

由於您學會了如何使用 requests,因此你可使用他們提供的迷人數據探索普遍的Web服務世界並構建出色的應用程序了。

代碼與藝術

關注公衆號 <代碼與藝術>,學習更多國外精品技術文章。

相關文章
相關標籤/搜索