a

Câu hỏi Cách nhanh nhất để gửi 100.000 yêu cầu HTTP bằng Python là gì?


Tôi đang mở một tệp có 100.000 url. Tôi cần gửi yêu cầu http tới từng url và in mã trạng thái. Tôi đang sử dụng Python 2.6, và cho đến nay nhìn vào nhiều cách khó hiểu Python thực hiện luồng / đồng thời. Tôi thậm chí đã nhìn vào con trăn sự đồng nhất thư viện, nhưng không thể tìm ra cách viết chương trình này một cách chính xác. Có ai đi qua một vấn đề tương tự? Tôi đoán chung tôi cần biết cách thực hiện hàng nghìn tác vụ bằng Python nhanh nhất có thể - tôi cho rằng điều đó có nghĩa là 'đồng thời'.


195
2018-04-13 19:19


gốc


Đảm bảo rằng bạn chỉ thực hiện yêu cầu HEAD (để bạn không tải xuống toàn bộ tài liệu). Xem: stackoverflow.com/questions/107405/… - Tarnay Kálmán
Tuyệt vời, Kalmi. Nếu tất cả Igor muốn là trạng thái của yêu cầu, các yêu cầu 100K này sẽ đi nhiều, nhanh hơn rất nhiều. Nhanh hơn nhiều. - Adam Crossland
Bạn không cần chủ đề cho điều này; cách hiệu quả nhất có thể sử dụng một thư viện không đồng bộ như Twisted. - jemfinch
đây là Ví dụ mã dựa trên gevent, xoắn và asyncio (được kiểm tra trên 1000000 yêu cầu) - jfs
@ TarnayKálmán của nó có thể cho requests.get và requests.head (nghĩa là yêu cầu trang so với yêu cầu đầu) để trả về các mã trạng thái khác nhau, vì vậy đây không phải là lời khuyên tốt nhất - AlexG


Các câu trả lời:


Giải pháp không xoắn:

from urlparse import urlparse
from threading import Thread
import httplib, sys
from Queue import Queue

concurrent = 200

def doWork():
    while True:
        url = q.get()
        status, url = getStatus(url)
        doSomethingWithResult(status, url)
        q.task_done()

def getStatus(ourl):
    try:
        url = urlparse(ourl)
        conn = httplib.HTTPConnection(url.netloc)   
        conn.request("HEAD", url.path)
        res = conn.getresponse()
        return res.status, ourl
    except:
        return "error", ourl

def doSomethingWithResult(status, url):
    print status, url

q = Queue(concurrent * 2)
for i in range(concurrent):
    t = Thread(target=doWork)
    t.daemon = True
    t.start()
try:
    for url in open('urllist.txt'):
        q.put(url.strip())
    q.join()
except KeyboardInterrupt:
    sys.exit(1)

Cái này nhanh hơn một chút so với giải pháp xoắn và sử dụng ít CPU hơn.


151
2018-04-14 05:22



Kalmi, giải pháp của bạn khá tốt. Các kết quả theo thời gian trên chạy chương trình của bạn với một tập tin của 100.000 url là: thực 5m23.863s người dùng 1m28.177s sys 2m34.299s Tuy nhiên, một câu hỏi tôi có là: không phải là populating hàng đợi với mỗi reduntant url và thêm chi phí? Tại sao không chỉ sinh ra các quá trình từ các url của bạn khi bạn đang đọc chúng từ các tập tin (mà không cần sử dụng một hàng đợi)? - Igor Ganapolsky
@Kalmi, tại sao bạn đặt Hàng đợi thành concurrent*2 ? - Marcel Wilson
Đừng quên đóng kết nối  conn.close(). Việc mở quá nhiều kết nối http có thể tạm dừng tập lệnh của bạn tại một số thời điểm và ăn bộ nhớ. - Aamir Adnan
@hyh, Queue mô-đun đã được đổi tên thành queue trong Python 3. Đây là mã Python 2. - Tarnay Kálmán
Bạn có thể nhanh hơn bao nhiêu nếu bạn muốn nói chuyện với máy chủ CÙNG mỗi lần, bằng cách kiên trì kết nối? Điều này thậm chí có thể được thực hiện trên các chủ đề, hoặc với một kết nối liên tục cho mỗi chủ đề? - mdurant


Một giải pháp sử dụng vòi rồng thư viện mạng không đồng bộ

from tornado import ioloop, httpclient

i = 0

def handle_request(response):
    print(response.code)
    global i
    i -= 1
    if i == 0:
        ioloop.IOLoop.instance().stop()

http_client = httpclient.AsyncHTTPClient()
for url in open('urls.txt'):
    i += 1
    http_client.fetch(url.strip(), handle_request, method='HEAD')
ioloop.IOLoop.instance().start()

41
2017-08-28 13:11



Và chúng sẽ chạy hoàn toàn song song? Hoặc là có một số hạn chế về số lượng các chủ đề? - Igor Ganapolsky
Mã này đang sử dụng I / O mạng không chặn và không có bất kỳ hạn chế nào. Nó có thể mở rộng đến hàng chục nghìn kết nối mở. Nó sẽ chạy trong một chủ đề duy nhất nhưng sẽ là một cách nhanh hơn sau đó bất kỳ giải pháp luồng. Thanh toán không bị chặn I / O en.wikipedia.org/wiki/Asynchronous_I/O - mher
Bạn có thể giải thích những gì đang xảy ra ở đây với biến i toàn cầu? Một số loại kiểm tra lỗi? - LittleBobbyTables
Nó là một truy cập để xác định khi nào thoát khỏi `` ioloop` - vì vậy khi bạn hoàn thành. - Michael Dorner
@AndrewScottEvans nó giả định rằng bạn đang sử dụng python 2,7 và proxy - Dejell


Chủ đề hoàn toàn không phải là câu trả lời ở đây. Họ sẽ cung cấp cả quá trình tắc nghẽn hạt nhân và quy trình, cũng như giới hạn thông lượng không được chấp nhận nếu mục tiêu tổng thể là "cách nhanh nhất".

Một chút của twisted và không đồng bộ HTTP khách hàng sẽ cung cấp cho bạn kết quả tốt hơn nhiều.


30
2018-04-13 20:14



ironfroggy: Tôi đang nghiêng về phía tình cảm của bạn. Tôi đã thử thực hiện giải pháp của mình với các chuỗi và hàng đợi (đối với các mutex tự động), nhưng bạn có thể tưởng tượng phải mất bao lâu để điền hàng đợi với 100.000 thứ ?? Tôi vẫn đang chơi xung quanh với các tùy chọn khác nhau và gợi ý của tất cả mọi người về chủ đề này, và có lẽ Twisted sẽ là một giải pháp tốt. - Igor Ganapolsky
Bạn có thể tránh populating một hàng đợi với 100k thứ. Chỉ cần xử lý từng mục một từ đầu vào của bạn, sau đó khởi chạy một chuỗi để xử lý yêu cầu tương ứng với mỗi mục. (Như tôi mô tả bên dưới, hãy sử dụng chuỗi trình khởi chạy để bắt đầu chuỗi yêu cầu HTTP khi số lượng chuỗi của bạn thấp hơn một số ngưỡng. Làm cho chuỗi ghi kết quả ra thành URL ánh xạ dict để phản hồi hoặc nối thêm tuples vào danh sách.) - Erik Garrison
ironfroggy: Ngoài ra, tôi rất tò mò về những gì mà bạn đã gặp phải khi sử dụng các chuỗi Python? Và làm thế nào để các chủ đề Python tương tác với hạt nhân hệ điều hành? - Erik Garrison
Đảm bảo bạn lắp đặt lò phản ứng epoll; nếu không bạn sẽ sử dụng select / poll, và nó sẽ rất chậm. Ngoài ra, nếu bạn thực sự cố gắng mở 100.000 kết nối đồng thời (giả sử chương trình của bạn được viết theo cách đó và URL trên các máy chủ khác nhau), bạn sẽ cần phải điều chỉnh hệ điều hành để không chạy các bộ mô tả tệp, các cổng tạm thời, v.v. (có thể dễ dàng hơn để đảm bảo rằng bạn không có nhiều hơn 10.000 kết nối xuất sắc cùng một lúc). - Mark Nottingham
erikg: bạn đã đề xuất một ý tưởng tuyệt vời. Tuy nhiên, kết quả tốt nhất tôi đã có thể đạt được với 200 chủ đề là khoảng. 6 phút. Tôi chắc chắn có nhiều cách để thực hiện điều này trong thời gian ngắn hơn ... Mark N: Nếu Twisted là cách tôi quyết định đi, thì lò phản ứng epoll chắc chắn sẽ hữu ích. Tuy nhiên, nếu kịch bản của tôi sẽ được chạy từ nhiều máy, điều đó không đòi hỏi phải cài đặt máy Twisted on EACH? Tôi không biết liệu tôi có thể thuyết phục ông chủ của tôi đi tuyến đường đó ... - Igor Ganapolsky


Sử dụng grequests , đó là sự kết hợp các yêu cầu + mô-đun Gevent.

GRequests cho phép bạn sử dụng các yêu cầu với Gevent để thực hiện các yêu cầu HTTP không đồng bộ một cách dễ dàng.

Cách sử dụng rất đơn giản:

import grequests

urls = [
   'http://www.heroku.com',
   'http://tablib.org',
   'http://httpbin.org',
   'http://python-requests.org',
   'http://kennethreitz.com'
]

Tạo một tập hợp các yêu cầu chưa được gửi:

>>> rs = (grequests.get(u) for u in urls)

Gửi tất cả cùng một lúc:

>>> grequests.map(rs)
[<Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>]

12
2017-07-14 07:41



gevent bây giờ hỗ trợ python 3 - Benjamin Toueg
grequests không phải là một phần của yêu cầu bình thường và có vẻ là phần lớn unmaintaned - Thom


Mọi thứ đã thay đổi khá nhiều kể từ năm 2010 khi bài đăng này được đăng và tôi chưa thử tất cả các câu trả lời khác nhưng tôi đã thử một vài câu hỏi và tôi thấy điều này phù hợp nhất với tôi bằng python3.6.

Tôi đã có thể tìm nạp khoảng ~ 150 tên miền duy nhất mỗi giây chạy trên AWS.

import pandas as pd
import concurrent.futures
import requests
import time

out = []
CONNECTIONS = 100
TIMEOUT = 5

tlds = open('../data/sample_1k.txt').read().splitlines()
urls = ['http://{}'.format(x) for x in tlds[1:]]

def load_url(url, timeout):
    ans = requests.head(url, timeout=timeout)
    return ans.status_code

with concurrent.futures.ThreadPoolExecutor(max_workers=CONNECTIONS) as executor:
    future_to_url = (executor.submit(load_url, url, TIMEOUT) for url in urls)
    time1 = time.time()
    for future in concurrent.futures.as_completed(future_to_url):
        try:
            data = future.result()
        except Exception as exc:
            data = str(type(exc))
        finally:
            out.append(data)

            print(str(len(out)),end="\r")

    time2 = time.time()

print(f'Took {time2-time1:.2f} s')
print(pd.Series(out).value_counts())

10
2017-09-10 19:13



Tôi chỉ hỏi vì tôi không biết nhưng liệu thứ tương lai này có được thay thế bằng async / await? - TankorSmash
Nó có thể, nhưng tôi đã tìm thấy ở trên để làm việc tốt hơn. bạn có thể sử dụng aiohttp nhưng nó không phải là một phần của lib chuẩn và đang thay đổi khá nhiều. Nó hoạt động nhưng tôi không thấy nó hoạt động tốt. Tôi nhận được tỷ lệ lỗi cao hơn khi tôi sử dụng nó và cho cuộc sống của tôi Tôi không thể làm cho nó hoạt động cũng như tương lai đồng thời mặc dù trong lý thuyết Có vẻ như nó sẽ làm việc tốt hơn, xem: stackoverflow.com/questions/45800857/… nếu bạn làm cho nó hoạt động tốt, hãy đăng câu trả lời của bạn để tôi có thể kiểm tra nó. - Glen Thompson
Tôi đã sử dụng câu trả lời này cho đến nay ngày hôm nay trong mã của tôi và nó hoạt động rất tốt, tôi chỉ tò mò thôi. - TankorSmash
Đây là một nitpick, nhưng tôi nghĩ nó sạch hơn rất nhiều time1 = time.time() ở đầu vòng lặp for và time2 = time.time() ngay sau vòng lặp for. - Matt M.


Nếu bạn đang tìm kiếm để có được hiệu suất tốt nhất có thể, bạn có thể muốn xem xét sử dụng I / O không đồng bộ thay vì chuỗi. Các chi phí liên quan đến hàng ngàn hệ điều hành chủ đề là không tầm thường và bối cảnh chuyển đổi trong trình thông dịch Python thêm nhiều hơn trên đầu trang của nó. Luồng chắc chắn sẽ nhận được công việc làm nhưng tôi nghi ngờ rằng một tuyến đường không đồng bộ sẽ cung cấp hiệu suất tổng thể tốt hơn.

Cụ thể, tôi muốn đề xuất ứng dụng web async trong thư viện Xoắn (http://www.twistedmatrix.com). Nó có một đường cong học tập được thừa nhận dốc nhưng nó khá dễ sử dụng khi bạn có được một xử lý tốt trên phong cách lập trình không đồng bộ của Twisted.

Một HowTo trên API khách hàng web không đồng bộ của Twisted có sẵn tại:

http://twistedmatrix.com/documents/current/web/howto/client.html


7
2018-04-13 20:12



Rakis: Tôi hiện đang xem xét I / O không đồng bộ và không chặn. Tôi cần phải học nó tốt hơn trước khi tôi thực hiện nó. Một bình luận tôi muốn thực hiện trên bài viết của bạn là nó là không thể (ít nhất là theo phân phối Linux của tôi) để sinh ra "hàng ngàn chủ đề hệ điều hành". Có một số lượng tối đa các luồng mà Python sẽ cho phép bạn sinh ra trước khi chương trình bị ngắt. Và trong trường hợp của tôi (trên CentOS 5) số lượng chủ đề tối đa là 303. - Igor Ganapolsky
Đó là điều tốt để biết. Tôi đã không bao giờ cố gắng sinh sản nhiều hơn một số ít trong Python cùng một lúc nhưng tôi đã có thể mong đợi để có thể tạo ra nhiều hơn trước khi nó bị đánh bom. - Rakis


Một cách tiếp cận tốt để giải quyết vấn đề này là đầu tiên viết mã cần thiết để có được một kết quả, sau đó kết hợp mã luồng để song song ứng dụng.

Trong một thế giới hoàn hảo, điều này đơn giản có nghĩa là đồng thời bắt đầu 100.000 luồng để xuất kết quả của chúng thành một từ điển hoặc danh sách để xử lý sau này, nhưng trong thực tế bạn bị giới hạn trong số lượng yêu cầu HTTP song song mà bạn có thể phát hành trong thời trang này. Tại địa phương, bạn có giới hạn về số lượng ổ cắm bạn có thể mở đồng thời, bao nhiêu chuỗi thực thi trình thông dịch Python của bạn sẽ cho phép. Từ xa, bạn có thể bị hạn chế về số lượng kết nối đồng thời nếu tất cả các yêu cầu là chống lại một máy chủ, hoặc nhiều. Những hạn chế này có lẽ sẽ đòi hỏi bạn viết kịch bản theo cách chỉ để thăm dò một phần nhỏ các URL tại một thời điểm (100, như một poster khác được đề cập, có thể là kích thước hồ bơi khá tốt, mặc dù bạn có thể thấy rằng có thể triển khai thành công nhiều hơn nữa).

Bạn có thể làm theo mẫu thiết kế này để giải quyết vấn đề trên:

  1. Bắt đầu một chuỗi khởi chạy các chuỗi yêu cầu mới cho đến khi số luồng hiện đang chạy (bạn có thể theo dõi chúng thông qua threading.active_count () hoặc bằng cách đẩy các đối tượng chuỗi vào cấu trúc dữ liệu)> = số yêu cầu đồng thời tối đa của bạn (100) , sau đó ngủ trong một thời gian chờ ngắn. Chuỗi này sẽ chấm dứt khi không có thêm URL nào để xử lý. Do đó, chủ đề sẽ tiếp tục thức dậy, khởi chạy chủ đề mới và ngủ cho đến khi bạn hoàn thành.
  2. Yêu cầu các luồng yêu cầu lưu trữ kết quả của chúng trong một số cấu trúc dữ liệu để thu hồi và xuất kết quả sau này. Nếu cấu trúc bạn lưu trữ kết quả là list hoặc là dict trong CPython, bạn có thể một cách an toàn nối thêm hoặc chèn các mục duy nhất từ ​​chủ đề của bạn mà không cần khóa, nhưng nếu bạn viết vào một tập tin hoặc yêu cầu trong tương tác dữ liệu phức tạp hơn bạn nên sử dụng khóa loại trừ lẫn nhau để bảo vệ trạng thái này khỏi tham nhũng.

Tôi sẽ đề nghị bạn sử dụng luồng mô-đun. Bạn có thể sử dụng nó để khởi động và theo dõi các chủ đề đang chạy. Hỗ trợ luồng của Python là trống, nhưng mô tả về vấn đề của bạn cho thấy rằng nó là hoàn toàn đủ cho nhu cầu của bạn.

Cuối cùng, nếu bạn muốn xem ứng dụng đơn giản của ứng dụng mạng song song được viết bằng Python, hãy xem ssh.py. Đó là một thư viện nhỏ sử dụng luồng Python để song song nhiều kết nối SSH. Thiết kế là đủ gần với yêu cầu của bạn mà bạn có thể tìm thấy nó là một nguồn lực tốt.


7
2018-04-13 20:09



erikg: sẽ ném trong một hàng đợi vào phương trình của bạn là hợp lý (cho khóa loại trừ lẫn nhau)? Tôi nghi ngờ rằng GIL của Python không hướng đến việc chơi với hàng ngàn chủ đề. - Igor Ganapolsky
Tại sao bạn cần khóa loại trừ lẫn nhau để ngăn chặn việc tạo quá nhiều luồng? Tôi nghi ngờ tôi hiểu sai thuật ngữ. Bạn có thể theo dõi các chủ đề đang chạy trong một chuỗi chủ đề, loại bỏ chúng khi chúng hoàn tất và thêm nhiều hơn đến giới hạn chuỗi đã nói. Nhưng trong một trường hợp đơn giản, chẳng hạn như câu hỏi bạn cũng có thể chỉ xem số lượng chủ đề đang hoạt động trong quy trình Python hiện tại, đợi cho đến khi nó rơi xuống dưới ngưỡng và khởi chạy nhiều chuỗi hơn đến ngưỡng như mô tả. Tôi đoán bạn có thể xem xét điều này một khóa ẩn, nhưng không có khóa rõ ràng được yêu cầu afaik. - Erik Garrison
erikg: không nhiều chủ đề chia sẻ nhà nước? Trên trang 305 trong cuốn sách của O'Reilly "Python cho quản trị hệ thống UNIX và Linux" nó nói: "... bằng cách sử dụng luồng không có hàng đợi làm cho nó phức tạp hơn nhiều người có thể xử lý thực tế. Đó là một ý tưởng tốt hơn để luôn luôn sử dụng hàng đợi Vì mô-đun hàng đợi cũng làm giảm bớt sự cần thiết phải bảo vệ dữ liệu một cách rõ ràng với các mutex vì bản thân hàng đợi đã được bảo vệ bởi một mutex. " Một lần nữa, tôi hoan nghênh quan điểm của bạn về điều này. - Igor Ganapolsky
Igor: Bạn hoàn toàn đúng là bạn nên sử dụng khóa. Tôi đã chỉnh sửa bài đăng để phản ánh điều này. Điều đó nói rằng, kinh nghiệm thực tế với python cho thấy rằng bạn không cần phải khóa cấu trúc dữ liệu mà bạn sửa đổi nguyên tử từ các chủ đề của bạn, chẳng hạn như bằng list.append hoặc bằng cách thêm một khóa băm. Lý do, tôi tin, là GIL, cung cấp các hoạt động như list.append với mức độ nguyên tử. Tôi hiện đang chạy một thử nghiệm để xác minh điều này (sử dụng 10k chủ đề để nối thêm số 0-9999 vào một danh sách, kiểm tra xem tất cả các phụ lục đã hoạt động). Sau gần 100 lần lặp lại, kiểm tra không thành công. - Erik Garrison
Igor: Tôi hỏi một câu hỏi khác về chủ đề này: stackoverflow.com/questions/2740435/… - Erik Garrison