راهنمای واکشی منابع اینترنتی با استفاده از بسته urllib¶
- نویسنده:
مقدمه¶
urllib.request یک ماژول پایتون برای واکشی URLها (نشانیهای یکدست منابع) است. این ماژول یک رابط بسیار ساده را در قالب تابع urlopen ارائه میدهد. این تابع میتواند URLها را با استفاده از پروتکلهای گوناگون واکشی کند. همچنین یک رابط کمی پیچیدهتر برای مدیریت موقعیتهای رایج ارائه میدهد — مانند احراز هویت پایه، کوکیها، پراکسیها و غیره. این موارد توسط اشیایی به نام handler و opener ارائه میشوند.
urllib.request از واکشی URLها برای بسیاری از «طرحهای URL» (URL schemes) با استفاده از پروتکلهای شبکهی مرتبط با آنها (مانند FTP، HTTP) پشتیبانی میکند؛ این طرحها با رشتهی پیش از ":" در URL شناسایی میشوند، برای مثال "ftp" طرحواره URL مربوط به "ftp://python.org/" است. این آموزش بر رایجترین حالت، یعنی HTTP، تمرکز دارد.
برای موقعیتهای ساده، استفاده از urlopen بسیار آسان است. اما به محض اینکه هنگام باز کردن URLهای HTTP با خطاها یا موارد غیربدیهی مواجه شوید، به درکی از پروتکل انتقال ابرمتن (HyperText Transfer Protocol) نیاز خواهید داشت. جامعترین و معتبرترین مرجع برای HTTP، RFC 2616 است. این سند فنی است و برای خواندن آسان در نظر گرفته نشده است. این راهنمای عملی (HOWTO) هدف دارد استفاده از urllib را با جزئیات کافی درباره HTTP نشان دهد تا شما را در پیشبرد کار یاری کند. هدف آن جایگزینی مستندات urllib.request نیست، بلکه مکملی برای آنها است.
واکشی URLها¶
سادهترین راه برای استفاده از urllib.request بهصورت زیر است:
import urllib.request
with urllib.request.urlopen('http://python.org/') as response:
html = response.read()
اگر میخواهید منبعی را از طریق URL بازیابی کنید و آن را در یک مکان موقت ذخیره کنید، میتوانید این کار را با توابع shutil.copyfileobj() و tempfile.NamedTemporaryFile() انجام دهید:
import shutil
import tempfile
import urllib.request
with urllib.request.urlopen('http://python.org/') as response:
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
shutil.copyfileobj(response, tmp_file)
with open(tmp_file.name) as html:
pass
بسیاری از کاربردهای urllib به همین سادگی خواهند بود (توجه داشته باشید که بهجای یک URL با 'http:' میتوانستیم از یک URL استفاده کنیم که با 'ftp:'، 'file:' و غیره شروع میشود). با این حال، هدف این آموزش توضیح موارد پیچیدهتر، با تمرکز بر HTTP است.
HTTP مبتنی بر درخواستها و پاسخها است؛ کلاینت درخواستها را ارسال میکند و سرورها پاسخها را ارسال میکنند. urllib.request این موضوع را با یک شیء Request بازتاب میدهد که نشاندهنده درخواست HTTP است که شما ارسال میکنید. در سادهترین حالت، شما یک شیء Request ایجاد میکنید که URL مورد نظر برای واکشی را مشخص میکند. فراخوانی urlopen با این شیء Request، یک شیء پاسخ برای URL درخواستشده برمیگرداند. این پاسخ یک شیء شبهپرونده است، به این معنا که میتوانید برای مثال .read() را روی پاسخ فراخوانی کنید:
import urllib.request
req = urllib.request.Request('http://python.org/')
with urllib.request.urlopen(req) as response:
the_page = response.read()
توجه داشته باشید که urllib.request از همان رابط Request برای مدیریت تمام طرحهای URL استفاده میکند. برای مثال، میتوانید یک درخواست FTP را به این صورت انجام دهید:
req = urllib.request.Request('ftp://example.com/')
در مورد HTTP، دو کار اضافی وجود دارد که اشیای Request به شما اجازه میدهند انجام دهید: اول، میتوانید دادههایی را بفرستید تا به سرور ارسال شوند. دوم، میتوانید اطلاعات اضافی («فراداده») دربارهی دادهها یا دربارهی خود درخواست را به سرور بفرستید - این اطلاعات بهصورت «سرآیندهای HTTP» ارسال میشود. بیایید هر یک از این موارد را بهنوبت بررسی کنیم.
داده¶
گاهی میخواهید دادهها را به یک URL بفرستید (اغلب URL به یک اسکریپت CGI (رابط دروازه مشترک) یا یک برنامه کاربردی وب دیگر اشاره دارد). در HTTP، این کار معمولاً با استفاده از آنچه به عنوان درخواست POST شناخته میشود انجام میشود. این معمولاً همان کاری است که مرورگر شما هنگام ارسال یک فرم HTML که در وب پر کردهاید انجام میدهد. لازم نیست همه درخواستهای POST از فرمها بیایند: شما میتوانید از یک POST برای انتقال دادههای دلخواه به برنامه خودتان استفاده کنید. در حالت رایج فرمهای HTML، دادهها باید به روش استانداردی کدگذاری شوند و سپس بهعنوان آرگومان data به شیء Request ارسال شوند. کدگذاری با استفاده از تابعی از کتابخانه urllib.parse انجام میشود.
import urllib.parse
import urllib.request
url = 'http://www.someserver.com/cgi-bin/register.cgi'
values = {'name' : 'Michael Foord',
'location' : 'Northampton',
'language' : 'Python' }
data = urllib.parse.urlencode(values)
data = data.encode('ascii') # data should be bytes
req = urllib.request.Request(url, data)
with urllib.request.urlopen(req) as response:
the_page = response.read()
توجه داشته باشید که گاهی به کدگذاریهای دیگری نیاز است (برای مثال برای بارگذاری پرونده از فرمهای HTML - برای جزئیات بیشتر HTML Specification, Form Submission را ببینید).
اگر آرگومان data را ارسال نکنید، urllib از یک درخواست GET استفاده میکند. یکی از تفاوتهای درخواستهای GET و POST این است که درخواستهای POST اغلب «عوارض جانبی» دارند: آنها وضعیت سامانه را به شکلی تغییر میدهند (برای مثال با ثبت سفارش در وبسایت برای یک hundredweight کنسرو اسپم که به درب منزل شما تحویل داده شود). اگرچه استاندارد HTTP بهوضوح مشخص میکند که درخواستهای POST در نظر گرفته شدهاند که همیشه عوارض جانبی ایجاد کنند و درخواستهای GET در نظر گرفته شدهاند که هرگز عوارض جانبی ایجاد نکنند، اما هیچچیز مانع از داشتن عوارض جانبی در یک درخواست GET، یا بدون عوارض جانبی بودن یک درخواست POST نمیشود. همچنین میتوان داده را در یک درخواست HTTP GET با کدگذاری آن در خود URL ارسال کرد.
این کار بهصورت زیر انجام میشود:
>>> import urllib.request
>>> import urllib.parse
>>> data = {}
>>> data['name'] = 'Somebody Here'
>>> data['location'] = 'Northampton'
>>> data['language'] = 'Python'
>>> url_values = urllib.parse.urlencode(data)
>>> print(url_values) # The order may differ from below.
name=Somebody+Here&language=Python&location=Northampton
>>> url = 'http://www.example.com/example.cgi'
>>> full_url = url + '?' + url_values
>>> data = urllib.request.urlopen(full_url)
توجه داشته باشید که URL کامل با افزودن یک ? به URL و سپس مقادیر کدگذاریشده ساخته میشود.
سرآیندها¶
در اینجا یک سرآیند HTTP خاص را بررسی میکنیم تا نحوه افزودن سرآیندها به درخواست HTTP شما را نشان دهیم.
برخی وبسایتها [1] دوست ندارند توسط برنامهها مرور شوند، یا نسخههای متفاوتی را به مرورگرهای مختلف [2] ارسال میکنند. بهطور پیشفرض، urllib خود را بهعنوان Python-urllib/x.y معرفی میکند (که x و y شمارههای اصلی و فرعی نسخه پایتون هستند، برای مثال Python-urllib/2.5)، که ممکن است باعث سردرگمی سایت شود، یا اصلاً کار نکند. روشی که یک مرورگر خود را معرفی میکند، از طریق سرآیند User-Agent [3] است. هنگام ایجاد یک شیء Request، میتوانید یک دیکشنری از سرآیندها را نیز ارسال کنید. مثال زیر همان درخواست بالا را انجام میدهد، اما خود را بهعنوان نسخهای از Internet Explorer [4] معرفی میکند.
import urllib.parse
import urllib.request
url = 'http://www.someserver.com/cgi-bin/register.cgi'
user_agent = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)'
values = {'name': 'Michael Foord',
'location': 'Northampton',
'language': 'Python' }
headers = {'User-Agent': user_agent}
data = urllib.parse.urlencode(values)
data = data.encode('ascii')
req = urllib.request.Request(url, data, headers)
with urllib.request.urlopen(req) as response:
the_page = response.read()
پاسخ همچنین دارای دو متد مفید است. بخش info and geturl را ببینید که پس از بررسی آنچه هنگام بروز مشکل رخ میدهد، آمده است.
مدیریت استثناها¶
urlopen زمانی که نتواند پاسخی را مدیریت کند، URLError را پرتاب میکند (اگرچه همانطور که در APIهای پایتون معمول است، استثناهای توکار مانند ValueError، TypeError و غیره نیز ممکن است پرتاب شوند).
HTTPError زیرکلاسی از URLError است که در مورد خاص URLهای HTTP پرتاب میشود.
کلاسهای استثنا از ماژول urllib.error اکسپورت میشوند.
URLError¶
اغلب، URLError به این دلیل پرتاب میشود که هیچ اتصال شبکهای وجود ندارد (مسیری به سرور مشخصشده وجود ندارد)، یا سرور مشخصشده وجود ندارد. در این حالت، استثنای پرتابشده دارای ویژگی 'reason' خواهد بود که یک تاپل شامل یک کد خطا و یک پیام خطای متنی است.
مثلاً
>>> req = urllib.request.Request('http://www.pretend_server.org')
>>> try: urllib.request.urlopen(req)
... except urllib.error.URLError as e:
... print(e.reason)
...
(4, 'getaddrinfo failed')
HTTPError¶
هر پاسخ HTTP از سرور حاوی یک «کد وضعیت» عددی است. گاهی کد وضعیت نشان میدهد که سرور قادر به برآوردن درخواست نیست. هندلرهای پیشفرض برخی از این پاسخها را برای شما مدیریت میکنند (برای مثال، اگر پاسخ یک «تغییر مسیر» باشد که از کلاینت میخواهد سند را از یک URL متفاوت واکشی کند، urllib آن را برای شما مدیریت میکند). برای آنهایی که قابل مدیریت نیستند، urlopen یک HTTPError پرتاب میکند. خطاهای رایج شامل '404' (صفحه پیدا نشد)، '403' (درخواست ممنوع) و '401' (نیاز به احراز هویت) هستند.
برای مراجعه به مرجعی دربارهی تمام کدهای خطای HTTP، بخش ۱۰ از RFC 2616 را ببینید.
نمونهی پرتابشدهی HTTPError یک ویژگی 'code' از نوع عدد صحیح خواهد داشت که متناظر با خطای ارسالشده از سوی سرور است.
کدهای خطا¶
از آنجا که هندلرهای پیشفرض تغییرمسیرها را مدیریت میکنند (کدهای بازهی ۳۰۰)، و کدهای بازهی ۱۰۰--۲۹۹ نشاندهندهی موفقیت هستند، معمولاً فقط کدهای خطا را در بازهی ۴۰۰--۵۹۹ مشاهده خواهید کرد.
http.server.BaseHTTPRequestHandler.responses یک دیکشنری مفید از کدهای پاسخ است که تمام کدهای پاسخ استفادهشده در RFC 2616 را نشان میدهد. گزیدهای از این دیکشنری در زیر نشان داده شده است
responses = {
...
<HTTPStatus.OK: 200>: ('OK', 'Request fulfilled, document follows'),
...
<HTTPStatus.FORBIDDEN: 403>: ('Forbidden',
'Request forbidden -- authorization will '
'not help'),
<HTTPStatus.NOT_FOUND: 404>: ('Not Found',
'Nothing matches the given URI'),
...
<HTTPStatus.IM_A_TEAPOT: 418>: ("I'm a Teapot",
'Server refuses to brew coffee because '
'it is a teapot'),
...
<HTTPStatus.SERVICE_UNAVAILABLE: 503>: ('Service Unavailable',
'The server cannot process the '
'request due to a high load'),
...
}
هنگامی که خطایی پرتاب میشود، سرور با بازگرداندن یک کد خطای HTTP و یک صفحهی خطا پاسخ میدهد. شما میتوانید از نمونهی HTTPError بهعنوان پاسخ برای صفحهی بازگرداندهشده استفاده کنید. این بدان معناست که علاوه بر ویژگی code، دارای متدهای read، geturl و info نیز هست، همانطور که توسط ماژول urllib.response بازگردانده میشوند:
>>> req = urllib.request.Request('http://www.python.org/fish.html')
>>> try:
... urllib.request.urlopen(req)
... except urllib.error.HTTPError as e:
... print(e.code)
... print(e.read())
...
404
b'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n\n\n<html
...
<title>Page Not Found</title>\n
...
جمعبندی¶
بنابراین اگر میخواهید برای HTTPError یا URLError آماده باشید، دو رویکرد اساسی وجود دارد. من رویکرد دوم را ترجیح میدهم.
عدد ۱¶
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
req = Request(someurl)
try:
response = urlopen(req)
except HTTPError as e:
print('The server couldn\'t fulfill the request.')
print('Error code: ', e.code)
except URLError as e:
print('We failed to reach a server.')
print('Reason: ', e.reason)
else:
# everything is fine
توجه
except HTTPError باید ابتدا قرار بگیرد، در غیر این صورت except URLError یک HTTPError را نیز میگیرد.
عدد ۲¶
from urllib.request import Request, urlopen
from urllib.error import URLError
req = Request(someurl)
try:
response = urlopen(req)
except URLError as e:
if hasattr(e, 'reason'):
print('We failed to reach a server.')
print('Reason: ', e.reason)
elif hasattr(e, 'code'):
print('The server couldn\'t fulfill the request.')
print('Error code: ', e.code)
else:
# everything is fine
info و geturl¶
پاسخ برگرداندهشده توسط urlopen (یا نمونه HTTPError) دارای دو متد مفید info() و geturl() است و در ماژول urllib.response تعریف شده است.
geturl - URL واقعی صفحه واکشیشده را برمیگرداند. این مفید است، زیرا ممکن است
urlopen(یا شیء opener مورد استفاده) از یک تغییر مسیر پیروی کرده باشد. ممکن است URL صفحه واکشیشده با URL درخواستشده یکسان نباشد.info - این یک شیء دیکشنریمانند برمیگرداند که صفحه واکشیشده را توصیف میکند، بهویژه سرآیندهای ارسالشده از سوی سرور. این در حال حاضر یک نمونه
http.client.HTTPMessageاست.
سرآیندهای معمول شامل 'Content-length'، 'Content-type' و غیره هستند. برای مشاهدهی فهرستی مفید از سرآیندهای HTTP با توضیحاتی مختصر دربارهی معنا و کاربرد آنها، Quick Reference to HTTP Headers را ببینید.
بازکنندهها و هندلرها¶
هنگامی که یک URL را واکشی میکنید، از یک بازکننده استفاده میکنید (نمونهای از urllib.request.OpenerDirector که شاید نام آن کمی گیجکننده است). معمولاً از بازکنندهی پیشفرض، از طریق urlopen، استفاده کردهایم، اما میتوانید بازکنندههای سفارشی ایجاد کنید. بازکنندهها از هندلرها استفاده میکنند. تمام «کار سنگین» را هندلرها انجام میدهند. هر هندلر میداند چگونه URL ها را برای یک طرحواره URL خاص (http، ftp و غیره) باز کند، یا چگونه جنبهای از باز کردن URL را مدیریت کند، برای مثال تغییرمسیرهای HTTP یا کوکیهای HTTP.
اگر بخواهید URLها را با هندلرهای خاصی نصبشده واکشی کنید، احتمالاً میخواهید بازکنندهها (openers) ایجاد کنید؛ برای مثال برای به دست آوردن بازکنندهای که کوکیها را مدیریت میکند، یا بازکنندهای که تغییرمسیرها (redirections) را مدیریت نمیکند.
برای ایجاد یک بازکننده، نمونهای از OpenerDirector بسازید و سپس .add_handler(some_handler_instance) را بهطور مکرر فراخوانی کنید.
بهعنوان جایگزین، میتوانید از build_opener استفاده کنید، که یک تابع تسهیلکننده برای ایجاد اشیای بازکننده تنها با یک فراخوانی تابع است. build_opener چندین هندلر را بهطور پیشفرض میافزاید، اما راهی سریع برای افزودن هندلرهای بیشتر و/یا جایگزینی هندلرهای پیشفرض فراهم میکند.
انواع دیگر هندلرها که ممکن است بخواهید، میتوانند پراکسیها، احراز هویت و دیگر موقعیتهای رایج اما کمی تخصصی را مدیریت کنند.
میتوان از install_opener برای تبدیل یک شیء opener به بازکنندهی پیشفرض (سراسری) استفاده کرد. این بدان معناست که فراخوانیهای urlopen از بازکنندهای که شما نصب کردهاید استفاده خواهند کرد.
اشیاء Opener یک متد open دارند که میتوان آن را مستقیماً برای واکشی نشانیهای اینترنتی به همان شیوهی تابع urlopen فراخوانی کرد: نیازی به فراخوانی install_opener نیست، مگر برای سهولت.
احراز هویت پایه¶
برای نشان دادن ایجاد و نصب یک هندلر، از HTTPBasicAuthHandler استفاده خواهیم کرد. برای بحث دقیقتر درباره این موضوع — شامل توضیحی درباره چگونگی کارکرد احراز هویت پایه (Basic Authentication) — Basic Authentication Tutorial را ببینید.
هنگامی که احراز هویت لازم باشد، سرور یک سرآیند (بههمراه کد خطای ۴۰۱) برای درخواست احراز هویت ارسال میکند. این سرآیند، طرحواره احراز هویت و یک «قلمرو» (realm) را مشخص میکند. سرآیند به این شکل است: WWW-Authenticate: SCHEME realm="REALM".
مثلاً
WWW-Authenticate: Basic realm="cPanel Users"
سپس کلاینت باید درخواست را دوباره ارسال کند و نام و گذرواژه مناسب برای قلمرو را بهعنوان یک سرآیند در درخواست درج کند. این «احراز هویت پایه» است. برای سادهسازی این فرایند، میتوانیم یک نمونه از HTTPBasicAuthHandler و یک بازکننده برای استفاده از این هندلر ایجاد کنیم.
HTTPBasicAuthHandler از یک شیء به نام مدیر گذرواژه برای مدیریت نگاشت URLها و قلمروها به گذرواژهها و نامهای کاربری استفاده میکند. اگر بدانید قلمرو چیست (از سرآیند احراز هویت ارسالشده توسط سرور)، میتوانید از HTTPPasswordMgr استفاده کنید. اغلب مهم نیست که قلمرو چیست. در این حالت، استفاده از HTTPPasswordMgrWithDefaultRealm مناسب است. این امکان را به شما میدهد که یک نام کاربری و گذرواژه پیشفرض برای یک URL مشخص کنید. این ترکیب در صورتی ارائه میشود که شما ترکیب جایگزینی برای یک قلمرو مشخص ارائه ندهید. ما این موضوع را با ارائه None بهعنوان آرگومان realm به متد add_password نشان میدهیم.
URL سطح بالا، نخستین URL است که به احراز هویت نیاز دارد. URLهای «عمیقتر» از URL دادهشده به .add_password() نیز مطابقت خواهند داشت.
# create a password manager
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
# Add the username and password.
# If we knew the realm, we could use it instead of None.
top_level_url = "http://example.com/foo/"
password_mgr.add_password(None, top_level_url, username, password)
handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
# create "opener" (OpenerDirector instance)
opener = urllib.request.build_opener(handler)
# use the opener to fetch a URL
opener.open(a_url)
# Install the opener.
# Now all calls to urllib.request.urlopen use our opener.
urllib.request.install_opener(opener)
توجه
در مثال بالا، ما فقط HTTPBasicAuthHandler خودمان را به build_opener ارائه دادیم. بهطور پیشفرض، بازکنندهها (openers) دارای هندلرهایی برای شرایط عادی هستند -- ProxyHandler (اگر تنظیمی برای پراکسی مانند متغیر محیطی http_proxy تنظیم شده باشد)، UnknownHandler، HTTPHandler، HTTPDefaultErrorHandler، HTTPRedirectHandler، FTPHandler، FileHandler، DataHandler، HTTPErrorProcessor.
top_level_url در واقع یا یک URL کامل است (شامل کامپوننت طرحواره 'http:'، نام میزبان و بهاختیار شماره پورت) مثلاً "http://example.com/" یا یک مرجع (authority) (یعنی نام میزبان، بهاختیار شامل شماره پورت) مثلاً "example.com" یا "example.com:8080" (مثال دوم شامل شماره پورت است). مرجع، در صورت وجود، نباید شامل کامپوننت «userinfo» باشد؛ برای مثال "joe:password@example.com" صحیح نیست.
پراکسیها¶
urllib تنظیمات پراکسی شما را بهصورت خودکار تشخیص میدهد و از آنها استفاده میکند. این کار از طریق ProxyHandler انجام میشود، که هنگام تشخیص یک تنظیم پراکسی، بخشی از زنجیرهی معمول هندلرها (handler chain) است. بهطور معمول، این موضوع خوبی است، اما مواردی وجود دارد که ممکن است مفید نباشد [5]. یکی از راهها برای انجام این کار، راهاندازی ProxyHandler خودمان، بدون تعریف هیچ پراکسیای است. این کار با مراحلی مشابه راهاندازی یک هندلر Basic Authentication انجام میشود:
>>> proxy_support = urllib.request.ProxyHandler({})
>>> opener = urllib.request.build_opener(proxy_support)
>>> urllib.request.install_opener(opener)
توجه
در حال حاضر urllib.request از واکشی آدرسهای https از طریق پراکسی پشتیبانی نمیکند. با این حال، میتوان این قابلیت را با گسترش urllib.request، همانطور که در دستور [6] نشان داده شده است، فعال کرد.
توجه
اگر متغیر REQUEST_METHOD تنظیم شده باشد، از HTTP_PROXY چشمپوشی میشود؛ مستندات getproxies() را ببینید.
سوکتها و لایهها¶
پشتیبانی پایتون برای واکشی منابع از وب، لایهبندیشده است. urllib از کتابخانهی http.client استفاده میکند که خود بهنوبهی خود از کتابخانهی socket استفاده میکند.
از پایتون ۲.۳ به بعد میتوانید مشخص کنید که یک سوکت پیش از پایان مهلت زمانی چقدر برای دریافت پاسخ منتظر بماند. این قابلیت میتواند در برنامههایی که باید صفحههای وب را دریافت کنند مفید باشد. بهطور پیشفرض، ماژول socket هیچ مهلت زمانی ندارد و ممکن است گیر کند. در حال حاضر، مهلت زمانی سوکت در سطحهای http.client یا urllib.request در دسترس نیست. بااینحال، میتوانید مهلت زمانی پیشفرض را بهصورت سراسری برای همهی سوکتها با استفاده از دستور زیر تنظیم کنید
import socket
import urllib.request
# timeout in seconds
timeout = 10
socket.setdefaulttimeout(timeout)
# this call to urllib.request.urlopen now uses the default timeout
# we have set in the socket module
req = urllib.request.Request('http://www.voidspace.org.uk')
response = urllib.request.urlopen(req)
پانویسها¶
این سند توسط جان لی بازبینی و اصلاح شد.