主頁 > 後端開發 > 爬蟲高性能相關

爬蟲高性能相關

2022-09-26 07:00:42 後端開發

一、背景知識

爬蟲的本質就是一個socket客戶端與服務端的通信程序,如果我們有多個url待爬取,只用一個執行緒且采用串行的方式執行,那只能等待爬取一個結束后才能繼續下一個,效率會非常低,

需要強調的是:對于單執行緒下串行N個任務,并不完全等同于低效,如果這N個任務都是純計算的任務,那么該執行緒對cpu的利用率仍然會很高,之所以單執行緒下串行多個爬蟲任務低效,是因為爬蟲任務是明顯的IO密集型程式,

二、同步、異步、回呼機制

1 同步呼叫:即提交一個任務后就在原地等待任務結束,等到拿到任務的結果后再繼續下一行代碼,效率低下

同步呼叫
import requests

def parse_page(res):
    print('決議 %s' %(len(res)))

def get_page(url):
    print('下載 %s' %url)
    response=requests.get(url)
    if response.status_code == 200:
        return response.text

urls=['https://www.baidu.com/','http://www.sina.com.cn/','https://www.python.org']
for url in urls:
    res=get_page(url) #呼叫一個任務,就在原地等待任務結束拿到結果后才繼續往后執行
    parse_page(res)

2 一個簡單的解決方案:多執行緒或多行程

多執行緒或多行程
# 在服務器端使用多執行緒(多行程),多執行緒(多行程)目的是讓每個連接都擁有獨立的執行緒(或行程),這樣任何一個連接的阻塞都不會影響其他的連接
# 開啟多行程或都執行緒的方式,我們是無法無限制地開啟多行程或多執行緒的:在遇到要同時回應成百上千路的連接請求,則無論多執行緒還是多行程都會嚴重占據系統資源,降低系統對外界回應效率,而且執行緒與行程本身也更容易進入假死狀態,
# IO密集型程式應該用多執行緒

import requests
from threading import Thread,current_thread

def parse_page(res):
    print('%s 決議 %s' %(current_thread().getName(),len(res)))

def get_page(url,callback=parse_page):
    print('%s 下載 %s' %(current_thread().getName(),url))
    response=requests.get(url)
    if response.status_code == 200:
        callback(response.text)

if __name__ == '__main__':
    urls=['https://www.baidu.com/','http://www.sina.com.cn/','https://www.python.org']
    for url in urls:
        t=Thread(target=get_page,args=(url,))
        t.start()

3 改進方案: 執行緒池或行程池+異步呼叫:提交一個任務后并不會等待任務結束,而是繼續下一行代碼

行程池或執行緒池:異步呼叫+回呼機制
# 很多程式員可能會考慮使用“執行緒池”或“連接池”,“執行緒池”旨在減少創建和銷毀執行緒的頻率,其維持一定合理數量的執行緒,并讓空閑的執行緒重新承擔新的執行任務,“連接池”維持連接的快取池,盡量重用已有的連接、減少創建和關閉連接的頻率,這兩種技術都可以很好的降低系統開銷,都被廣泛應用很多大型系統,如websphere、tomcat和各種資料庫等

#IO密集型程式應該用多執行緒,所以此時我們使用執行緒池

import requests
from threading import current_thread
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

def parse_page(res):
    res=res.result()
    print('%s 決議 %s' %(current_thread().getName(),len(res)))

def get_page(url):
    print('%s 下載 %s' %(current_thread().getName(),url))
    response=requests.get(url)
    if response.status_code == 200:
        return response.text

if __name__ == '__main__':
    urls=['https://www.baidu.com/','http://www.sina.com.cn/','https://www.python.org']


pool=ThreadPoolExecutor(50)
# pool=ProcessPoolExecutor(50)
for url in urls:
    pool.submit(get_page,url).add_done_callback(parse_page)

pool.shutdown(wait=True)

改進后方案其實也存在著問題:
“執行緒池”和“連接池”技術也只是在一定程度上緩解了頻繁呼叫IO介面帶來的資源占用,而且,所謂“池”始終有其上限,當請求大大超過上限時,“池”構成的系統對外界的回應并不比沒有池的時候效果好多少,所以使用“池”必須考慮其面臨的回應規模,并根據回應規模調整“池”的大小

三、高性能

  對應上例中的所面臨的可能同時出現的上千甚至上萬次的客戶端請求,“執行緒池”或“連接池”或許可以緩解部分壓力,但是不能解決所有問題,總之,多執行緒模型可以方便高效的解決小規模的服務請求,但面對大規模的服務請求,多執行緒模型也會遇到瓶頸,可以用非阻塞介面來嘗試解決這個問題,
上述無論哪種解決方案其實沒有解決一個性能相關的問題:IO阻塞,無論是多行程還是多執行緒,在遇到IO阻塞時都會被作業系統強行剝奪走CPU的執行權限,程式的執行效率因此就降低了下來,

  解決這一問題的關鍵在于,我們自己從應用程式級別檢測IO阻塞然后切換到我們自己程式的其他任務執行,這樣把我們程式的IO降到最低,我們的程式處于就緒態就會增多,以此來迷惑作業系統,作業系統便以為我們的程式是IO比較少的程式,從而會盡可能多的分配CPU給我們,這樣也就達到了提升程式執行效率的目的

1 在python3.3之后新增了asyncio模塊,可以幫我們檢測IO(只能是網路IO),實作應用程式級別的切換

基本使用
import asyncio

@asyncio.coroutine
def task(task_id,senconds):
    print('%s is start' %task_id)
    yield from asyncio.sleep(senconds) #只能檢測網路IO,檢測到IO后切換到其他任務執行
    print('%s is end' %task_id)

tasks=[task(task_id="任務1",senconds=3),task("任務2",2),task(task_id="任務3",senconds=1)]

loop=asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

2 但asyncio模塊只能發tcp級別的請求,不能發http協議,因此,在我們需要發送http請求的時候,需要我們自定義http報頭

asyncio+自定義http協議報頭
import asyncio
import requests
import uuid
user_agent='Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.221 Safari/537.36 SE 2.X MetaSr 1.0'

def parse_page(host,res):
    print('%s 決議結果 %s' %(host,len(res)))
    with open('%s.html' %(uuid.uuid1()),'wb') as f:
        f.write(res)

@asyncio.coroutine
def get_page(host,port=80,url='/',callback=parse_page,ssl=False):
    print('下載 http://%s:%s%s' %(host,port,url))


#步驟一(IO阻塞):發起tcp鏈接,是阻塞操作,因此需要yield from
if ssl:
    port=443
recv,send=yield from asyncio.open_connection(host=host,port=443,ssl=ssl)

# 步驟二:封裝http協議的報頭,因為asyncio模塊只能封裝并發送tcp包,因此這一步需要我們自己封裝http協議的包
request_headers="""GET %s HTTP/1.0\r\nHost: %s\r\nUser-agent: %s\r\n\r\n""" %(url,host,user_agent)
# requset_headers="""POST %s HTTP/1.0\r\nHost: %s\r\n\r\nname=egon&password=123""" % (url, host,)
request_headers=request_headers.encode('utf-8')

# 步驟三(IO阻塞):發送http請求包
    send.write(request_headers)
    yield from send.drain()

# 步驟四(IO阻塞):接收回應頭
while True:
    line=yield from recv.readline()
    if line == b'\r\n':
        break
    print('%s Response headers:%s' %(host,line))

# 步驟五(IO阻塞):接收回應體
text=yield from recv.read()

# 步驟六:執行回呼函式
    callback(host,text)

# 步驟七:關閉套接字
send.close() #沒有recv.close()方法,因為是四次揮手斷鏈接,雙向鏈接的兩端,一端發完資料后執行send.close()另外一端就被動地斷開

if __name__ == '__main__':
    tasks=[
        get_page('www.baidu.com',url='/s?wd=美女',ssl=True),
        get_page('www.cnblogs.com',url='/',ssl=True),
    ]

loop=asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

3 自定義http報頭多少有點麻煩,于是有了aiohttp模塊,專門幫我們封裝http報頭,然后我們還需要用asyncio檢測IO實作切換

asyncio+aiohttp
import aiohttp
import asyncio

@asyncio.coroutine
def get_page(url):
    print('GET:%s' %url)
    response=yield from aiohttp.request('GET',url)


data=https://www.cnblogs.com/cqzlei/archive/2022/09/25/yield from response.read()

print(url,data)
response.close()
return 1


tasks=[
    get_page('https://www.python.org/doc'),
    get_page('https://www.cnblogs.com/linhaifeng'),
    get_page('https://www.openstack.org')
]

loop=asyncio.get_event_loop()
results=loop.run_until_complete(asyncio.gather(*tasks))
loop.close()

print('=====>',results) #[1, 1, 1]

4 此外,還可以將requests.get函式傳給asyncio,就能夠被檢測了

asyncio+requests模塊
import requests
import asyncio

@asyncio.coroutine
def get_page(func,*args):
    print('GET:%s' %args[0])
    loog=asyncio.get_event_loop()
    furture=loop.run_in_executor(None,func,*args)
    response=yield from furture

print(response.url,len(response.text))
return 1

tasks=[
    get_page(requests.get,'https://www.python.org/doc'),
    get_page(requests.get,'https://www.cnblogs.com/linhaifeng'),
    get_page(requests.get,'https://www.openstack.org')
]

loop=asyncio.get_event_loop()
results=loop.run_until_complete(asyncio.gather(*tasks))
loop.close()

print('=====>',results) #[1, 1, 1]

5 還有之前在協程時介紹的gevent模塊

gevent+requests
from gevent import monkey;monkey.patch_all()
import gevent
import requests

def get_page(url):
    print('GET:%s' %url)
    response=requests.get(url)
    print(url,len(response.text))
    return 1

# g1=gevent.spawn(get_page,'https://www.python.org/doc')
# g2=gevent.spawn(get_page,'https://www.cnblogs.com/linhaifeng')
# g3=gevent.spawn(get_page,'https://www.openstack.org')
# gevent.joinall([g1,g2,g3,])
# print(g1.value,g2.value,g3.value) #拿到回傳值

#協程池
from gevent.pool import Pool
pool=Pool(2)
g1=pool.spawn(get_page,'https://www.python.org/doc')
g2=pool.spawn(get_page,'https://www.cnblogs.com/linhaifeng')
g3=pool.spawn(get_page,'https://www.openstack.org')
gevent.joinall([g1,g2,g3,])
print(g1.value,g2.value,g3.value) #拿到回傳值

6 封裝了gevent+requests模塊的grequests模塊

grequests
#pip3 install grequests

import grequests

request_list=[
    grequests.get('https://wwww.xxxx.org/doc1'),
    grequests.get('https://www.cnblogs.com/linhaifeng'),
    grequests.get('https://www.openstack.org')
]

##### 執行并獲取回應串列 #####
# response_list = grequests.map(request_list)
# print(response_list)

##### 執行并獲取回應串列(處理例外) #####
def exception_handler(request, exception):
    # print(request,exception)
    print("%s Request failed" %request.url)

response_list = grequests.map(request_list, exception_handler=exception_handler)
print(response_list)

7 twisted:是一個網路框架,其中一個功能是發送異步請求,檢測IO并自動切換

twisted基本用法
# 問題一:error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
https://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
pip3 install C:\Users\Administrator\Downloads\Twisted-17.9.0-cp36-cp36m-win_amd64.whl
pip3 install twisted

# 問題二:ModuleNotFoundError: No module named 'win32api'
https://sourceforge.net/projects/pywin32/files/pywin32/

# 問題三:openssl
pip3 install pyopenssl


# twisted基本用法
from twisted.web.client import getPage,defer
from twisted.internet import reactor

def all_done(arg):
    # print(arg)
    reactor.stop()

def callback(res):
    print(res)
    return 1

defer_list=[]
urls=[
    'http://www.baidu.com',
    'http://www.bing.com',
    'https://www.python.org',
]
for url in urls:
    obj=getPage(url.encode('utf=-8'),)
    obj.addCallback(callback)
    defer_list.append(obj)

defer.DeferredList(defer_list).addBoth(all_done)

reactor.run()


# twisted的getPage的詳細用法
from twisted.internet import reactor
from twisted.web.client import getPage
import urllib.parse

def one_done(arg):
    print(arg)
    reactor.stop()

post_data = https://www.cnblogs.com/cqzlei/archive/2022/09/25/urllib.parse.urlencode({'check_data': 'adf'})
post_data = https://www.cnblogs.com/cqzlei/archive/2022/09/25/bytes(post_data, encoding='utf8')
headers = {b'Content-Type': b'application/x-www-form-urlencoded'}
response = getPage(bytes('http://dig.chouti.com/login', encoding='utf8'),
                   method=bytes('POST', encoding='utf8'),
                   postdata=https://www.cnblogs.com/cqzlei/archive/2022/09/25/post_data,
                   cookies={},
                   headers=headers)
response.addBoth(one_done)

reactor.run()

8 tornado

tornado用法
from tornado.httpclient import AsyncHTTPClient
from tornado.httpclient import HTTPRequest
from tornado import ioloop

def handle_response(response):
    """
    處理回傳值內容(需要維護計數器,來停止IO回圈),呼叫 ioloop.IOLoop.current().stop()
    :param response:
    :return:
    """
    if response.error:
        print("Error:", response.error)
    else:
        print(response.body)

def func():
    url_list = [
        'http://www.baidu.com',
        'http://www.bing.com',
    ]
    for url in url_list:
        print(url)
        http_client = AsyncHTTPClient()
        http_client.fetch(HTTPRequest(url), handle_response)

ioloop.IOLoop.current().add_callback(func)
ioloop.IOLoop.current().start()


# 發現上例在所有任務都完畢后也不能正常結束,為了解決該問題,讓我們來加上計數器
from tornado.httpclient import AsyncHTTPClient
from tornado.httpclient import HTTPRequest
from tornado import ioloop

count=0

def handle_response(response):
    """
    處理回傳值內容(需要維護計數器,來停止IO回圈),呼叫 ioloop.IOLoop.current().stop()
    :param response:
    :return:
    """
    if response.error:
        print("Error:", response.error)
    else:
        print(len(response.body))


global count
count-=1 #完成一次回呼,計數減1
if count == 0:
    ioloop.IOLoop.current().stop() 

def func():
    url_list = [
        'http://www.baidu.com',
        'http://www.bing.com',
    ]

global count
for url in url_list:
    print(url)
    http_client = AsyncHTTPClient()
    http_client.fetch(HTTPRequest(url), handle_response)
    count+=1 #計數加1


ioloop.IOLoop.current().add_callback(func)
ioloop.IOLoop.current().start()

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/509491.html

標籤:其他

上一篇:Invalid bound statement (not found)出現原因和解決方法

下一篇:拉格朗日插值原理及實作(Python)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more