برنامه‌نویسی پایتون فهرست مطالب
فصل ۲۸

بسته‌ها، REST API و XML

تا اینجا فقط از کتابخانه استاندارد استفاده کردیم. حالا از پایتون بیرون می‌رویم: بسته نصب می‌کنیم، به اینترنت وصل می‌شویم و داده واقعی می‌گیریم.

در این فصل می‌خوانید
  1. چرا محیط مجازی
  2. دستورهای pip
  3. فایل requirements.txt
  4. اولین بسته نصب‌شده
  5. چند بسته محبوب
  6. وب‌سرویس REST چیست
  7. کتابخانه requests
  8. وب‌سرویس صفحه‌بندی‌شده
  9. ساختار XML
  10. خواندن XML
  11. نوشتن XML
  12. خواندن خوراک RSS

۱. چرا محیط مجازی

فرض کنید دو پروژه دارید. یکی با نسخه ۱ یک کتابخانه کار می‌کند و دیگری با نسخه ۲. اگر همه چیز را روی پایتون سیستم نصب کنید، نمی‌شود هر دو را داشت. محیط مجازی پوشه‌ای خصوصی است که نسخه‌ای از پایتون و فقط بسته‌های همین پروژه را در خود دارد.

python -m venv .venv

ساختن محیط مجازی. پوشه‌ای به نام venv. ساخته می‌شود

# Windows (Command Prompt)
.venv\Scripts\activate

# Windows (PowerShell)
Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned
.\.venv\Scripts\activate

# Linux, macOS, Git Bash
source .venv/bin/activate

فعال کردن محیط

بعد از فعال شدن، نام محیط جلوی خط فرمان ظاهر می‌شود: (.venv). برای خروج، فقط deactivate بنویسید.

سه عادت خوب

۲. دستورهای pip

python -m pip install --upgrade pip
pip install MODULE_NAME
pip install MODULE_NAME==1.2.3      # a precise version
pip list                            # what is installed
pip show MODULE_NAME                # details of one package
pip uninstall MODULE_NAME

دستورهای اصلی pip

pip ابزار نصب بسته‌های پایتون است و بسته‌ها را از مخزن جهانی PyPI می‌گیرد. بیش از پانصد هزار بسته آنجا هست.

۳. فایل requirements.txt

pip freeze > requirements.txt       # write down everything that is installed
pip install -r requirements.txt     # rebuild the same environment elsewhere

ذخیره و بازسازی محیط

این فایل فهرست دقیق بسته‌ها و نسخه‌هایشان است. هر کسی که پروژه شما را بگیرد، با یک دستور دقیقاً همان محیط را می‌سازد. این استاندارد جهانی پروژه‌های پایتون است.

باگی که در همه فایل‌های اصلی این دوره بود

در نسخه اصلی، فایل‌های requirements.txt با انکدینگ UTF-16 ذخیره شده بودند، چون در ویندوز با > در PowerShell ساخته شده بودند. نتیجه این بود که pip install -r requirements.txt اصلاً نمی‌توانست بخواندشان. اگر با این مشکل روبه‌رو شدید، فایل را در VS Code باز کنید و انکدینگش را به UTF-8 تغییر دهید.

۴. اولین بسته نصب‌شده

'''A first third-party package: the Dilami (Gilaki) calendar.

Before running:
    python -m venv .venv
    (activate it)
    pip install -r requirements.txt
'''

from datetime import datetime

# A package that is not installed raises ModuleNotFoundError, so we give a
# clear message instead of a traceback.
try:
    from dilami_calendar import DilamiDatetime
except ModuleNotFoundError:
    raise SystemExit('Run "pip install -r requirements.txt" first.')


def main() -> None:
    print('The Dilami date of today:')
    print(DilamiDatetime.now())

    print('Converting a Gregorian date to Dilami:')
    gregorian_datetime = datetime(2026, 7, 19)
    dilami_datetime = DilamiDatetime(gregorian_datetime)

    print(f'    Year:  {dilami_datetime.year}')
    print(f'    Month: {dilami_datetime.month}')
    print(f'    Day:   {dilami_datetime.day}')

تقویم دیلمی: نمونه 01_venv_and_pip

تقویم دیلمی، تقویم بومی گیلان است و بسته‌اش را یک برنامه‌نویس ایرانی نوشته است. نمونه خوبی است از اینکه در PyPI برای هر نیازی چیزی پیدا می‌شود.

الگوی بررسی نصب بودن بسته

آن try دور import عادت خوبی است: به جای اینکه کاربر یک ردیابی ترسناک ببیند، جمله روشنی می‌خواند که به او می‌گوید چه کار کند. در همه نمونه‌های این فصل همین الگو به کار رفته است.

۵. چند بسته محبوب

بستهوظیفه آنپوشه نمونه
matplotlibرسم نمودار02_matplotlib
requestsکار با وب و وب‌سرویس03_requests_basics
Pillowپردازش تصویر04_pillow
pandasتحلیل داده و جدول05_pandas

این چهار بسته، پرکاربردترین‌های دنیای پایتون‌اند. هر کدام خودشان یک دوره کامل می‌طلبند؛ اینجا فقط می‌بینید که وجود دارند و چطور نصب و استفاده می‌شوند.

۶. وب‌سرویس REST چیست

API یعنی راهی که یک برنامه از راه اینترنت به برنامه دیگری داده می‌دهد. سبک REST رایج‌ترین شکلش است و واژگانش این مواردست:

مفهومیعنی
URLکدام منبع را می‌خواهیم
متدGET خواندن، POST ساختن، PUT تغییر، DELETE حذف
کد وضعیت۲۰۰ موفق، ۴۰۴ پیدا نشد، ۵۰۰ خطای سرور
بدنهخود داده، تقریباً همیشه JSON

۷. کتابخانه requests

try:
    import requests
except ModuleNotFoundError:
    raise SystemExit('Run "pip install -r requirements.txt" first.')


def main() -> None:
    # 1. A simple GET, with a timeout so the program cannot hang for ever.
    try:
        response = requests.get('https://api.github.com', timeout=10)
        response.raise_for_status()      # raises when the status is 4xx or 5xx
    except requests.RequestException as e:
        print('The request failed:', e)
        return

    print('Status code:', response.status_code)      # 200 means success

    # 2. The answer parsed as JSON -> a Python dictionary.
    data = response.json()
    print('Example field user_url:', data['current_user_url'])

    # 3. Query parameters: requests builds the ?q=python&sort=stars part for us.
    params = {'q': 'python', 'sort': 'stars'}
    search = requests.get(
        'https://api.github.com/search/repositories',
        params=params,
        timeout=10,
    )
    search.raise_for_status()

    result = search.json()
    print('Number of search results:', result['total_count'])
    print('The final URL was:', search.url)

    for repo in result['items'][:5]:
        print(f"  {repo['full_name']:<40} {repo['stargazers_count']:>8} stars")

اولین درخواست: نمونه 03_requests_basics

چهار قانون کار با اینترنت

۸. وب‌سرویس صفحه‌بندی‌شده

هیچ وب‌سرویسی ده هزار رکورد را یکجا نمی‌دهد. داده‌ها صفحه‌بندی می‌شوند و شما باید صفحه به صفحه بگیریدشان.

def fetch_all_posts(endpoint: str) -> list[dict]:
    'Download every page of posts and return them in one list.'
    all_posts: list[dict] = []
    page = 1

    while True:
        try:
            response = requests.get(
                endpoint,
                params={'page': page, 'per_page': PER_PAGE},
                timeout=TIMEOUT,
            )
            response.raise_for_status()
        except requests.RequestException as e:
            print(f'Error fetching page {page}: {e}')
            break

        posts = response.json()
        if not posts:                 # an empty page means we are done
            break

        all_posts.extend(posts)

        # The API tells us how many pages exist, in a header.
        total_pages = int(response.headers.get('X-WP-TotalPages', 1))
        if page >= total_pages:
            break

        page += 1

    return all_posts

گرفتن همه صفحه‌ها: نمونه 09_rest_api

شرح مثال

۹. ساختار XML

XML قالب قدیمی‌تری برای داده ساختاریافته است. هنوز در خیلی جاها استفاده می‌شود: RSS، فایل‌های آفیس، تنظیمات جاوا، و فایل‌های ui. که در فصل سی و ششم می‌بینید.

<?xml version="1.0" encoding="UTF-8"?>
<products>
    <product id="101" name="Intel Core i7-13700K" manufacturer="Intel">
        <tags>
            <tag>CPU</tag>
            <tag>13th Gen</tag>
        </tags>
        <categories>
            <category>Processor</category>
        </categories>
    </product>
</products>

ساختار XML: فایل products.xml

قواعد XML:

XMLJSON
حجمبیشتر، تگ‌ها تکرار می‌شوندکمتر
خواناییمتوسطخوب
ویژگی و متنهر دو داردفقط کلید و مقدار
امروز کجاRSS، آفیس، سیستم‌های قدیمیوب و موبایل
یک فایل خراب برای یادگیری

در پوشه 06_xml_basics فایلی به نام broken.xml هست که عمداً دو ریشه دارد و ماژول XML نمی‌تواند بخواندش. کنارش fixed.xml نسخه درستش است. این همان اشتباهی بود که در فایل اصلی دوره وجود داشت و به جای حذف، به یک درس تبدیل شد.

۱۰. خواندن XML

'''The useful methods of ElementTree:
    ET.parse(file)      -> a tree
    tree.getroot()      -> the root element
    element.find(tag)   -> the FIRST child with that tag (None when missing)
    element.findall(tag)-> a list of every child with that tag
    element.get(name)   -> the value of an ATTRIBUTE (None when missing)
    element.text        -> the text between the tags
'''

import xml.etree.ElementTree as ET


def parse_xml_to_list(filename: str) -> list[dict]:
    'Read products.xml and return a list of dictionaries.'
    tree = ET.parse(filename)
    root = tree.getroot()

    products: list[dict] = []

    for product_elem in root.findall('product'):
        product: dict = {}

        # The attributes of <product ...>
        product['id'] = int(product_elem.get('id'))
        product['name'] = product_elem.get('name')
        product['manufacturer'] = product_elem.get('manufacturer')

        # The nested <tags><tag>...</tag></tags>
        # find() returns None when the element is missing, so we must test it.
        tags_elem = product_elem.find('tags')
        tag_list: list[str] = []
        if tags_elem is not None:
            for tag in tags_elem.findall('tag'):
                tag_list.append(tag.text)
        product['tags'] = tag_list

        products.append(product)

    return products

خواندن XML: نمونه 07_read_xml

دام مقایسه با None

متد find اگر چیزی پیدا نکند None برمی‌گرداند. حتماً بنویسید if elem is not None: و نه if elem:. دلیلش عجیب است: یک عنصر XML بدون فرزند، در شرط نادرست حساب می‌شود، حتی اگر واقعاً وجود داشته باشد. این یکی از مواردی است که در فصل بیست و پنجم درباره تفاوت درستی‌پذیری و is not None گفتیم.

استراتژی کلی: XML را در همان ابتدا به ساختارهای معمولی پایتون تبدیل کنید. بعد از آن، همه ابزارهایی که بلدید کار می‌کنند و می‌توانید در یک خط به JSON تبدیلش کنید.

۱۱. نوشتن XML

'''Building an XML document with ElementTree.

    ET.Element(tag)              -> the root
    ET.SubElement(parent, tag)   -> a child
    element.text = '...'         -> its text (must be a STRING)
    element.set('name', value)   -> an attribute
'''

import xml.etree.ElementTree as ET


def news_list_to_xml(news_list: list[dict]) -> ET.ElementTree:
    'Turn the list of articles into an XML tree.'
    root = ET.Element('news')

    for article in news_list:
        article_elem = ET.SubElement(root, 'article')

        # The id fits better as an attribute than as a child element.
        article_elem.set('id', str(article['news_id']))

        # .text must always be a string - that is why we call str().
        ET.SubElement(article_elem, 'title').text = article['title']
        ET.SubElement(article_elem, 'content').text = article['content']

        tags_elem = ET.SubElement(article_elem, 'tags')
        for tag in article['tags']:
            ET.SubElement(tags_elem, 'tag').text = tag

        comments_elem = ET.SubElement(article_elem, 'comments')
        for comment in article['comments']:
            comment_elem = ET.SubElement(comments_elem, 'comment')
            ET.SubElement(comment_elem, 'user').text = comment['user']
            ET.SubElement(comment_elem, 'likes').text = str(comment['likes'])

    return ET.ElementTree(root)


def main() -> None:
    tree = news_list_to_xml(create_sample_news())

    # The simple modern way (Python 3.9+):
    ET.indent(tree, space='  ')
    tree.write('news_simple.xml', encoding='utf-8', xml_declaration=True)

نوشتن XML: نمونه 08_write_xml

نکته مهم: text و مقدار ویژگی‌ها حتماً باید رشته باشند. عدد دادن خطا ایجاد می‌کند، پس همیشه str() بزنید. تابع ET.indent هم از پایتون ۳٫۹ اضافه شده و فایل را خوانا و تورفته می‌کند؛ پیش از آن باید از minidom استفاده می‌شد.

۱۲. خواندن خوراک RSS

RSS یک فایل XML است که تقریباً هر سایت خبری و وبلاگی منتشر می‌کند، معمولاً در نشانی feed/. این آخرین مثال، همه چیز این فصل را کنار هم می‌گذارد.

import json
import xml.etree.ElementTree as ET
import requests

SITE_URL = 'https://kadoosedu.ir'
OUTPUT_FILE = 'rss_posts.json'


def text_of(parent: ET.Element, tag: str) -> str:
    'Return the text of a child element, or an empty string when it is missing.'
    element = parent.find(tag)
    if element is None or element.text is None:
        return ''
    return element.text.strip()


def parse_rss(xml_content: bytes) -> list[dict]:
    '''Parse the XML of an RSS feed.

    The structure is always:  rss > channel > item*
    '''
    # fromstring() parses a string/bytes; parse() parses a file.
    root = ET.fromstring(xml_content)

    channel = root.find('channel')
    if channel is None:
        raise ValueError('this document is not an RSS feed')

    posts: list[dict] = []

    for item in channel.findall('item'):
        posts.append({
            'title': text_of(item, 'title'),
            'link': text_of(item, 'link'),
            'description': text_of(item, 'description'),
            'pubDate': text_of(item, 'pubDate'),
        })

    return posts


def main() -> None:
    feed_url = f'{SITE_URL}/feed/'

    try:
        response = requests.get(feed_url, timeout=15)
        response.raise_for_status()
    except requests.RequestException as e:
        print(f'Error fetching the RSS feed: {e}')
        return

    try:
        posts = parse_rss(response.content)
    except (ET.ParseError, ValueError) as e:
        print(f'The feed could not be parsed: {e}')
        return

    for post in posts[:5]:
        print(f"  - {post['title']}")

    with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
        json.dump(posts, f, ensure_ascii=False, indent=2)

خواندن RSS: نمونه 10_read_rss

شرح مثال

فایل‌های تمرینی این فصل

پوشه S28-packages-rest-api-and-xml شامل ده پوشه نمونه، از 01_venv_and_pip تا 10_read_rss. برای اجرای هر کدام، اول محیط مجازی بسازید و requirements.txt همان پوشه را نصب کنید.

خلاصه فصل

ابزاروظیفه آن
python -m venv .venvساختن محیط مجازی
activate و deactivateورود و خروج از محیط
pip install list showمدیریت بسته‌ها
pip freeze > requirements.txtثبت بسته‌های پروژه
pip install -r requirements.txtبازسازی محیط
requests.get(url, timeout=...)درخواست وب. timeout اجباری
raise_for_status()تبدیل خطای سرور به استثنا
response.json()تبدیل پاسخ به شیء پایتون
params={...}پارامترهای پرس‌وجو
ET.parse و getrootخواندن XML از فایل
find findall get textگشتن در درخت XML
ET.Element و SubElementساختن XML
ET.indentتورفته کردن خروجی XML
RSSخوراک XML سایت‌ها: rss سپس channel سپس item

تمرین‌های فصل بیست و هشت

  1. برای یک پروژه تازه محیط مجازی بسازید، requests را در آن نصب کنید و requirements.txt تولید کنید.
  2. محیط را پاک کنید، از روی requirements.txt دوباره بسازیدش و ببینید برنامه هنوز کار می‌کند.
  3. یک وب‌سرویس عمومی رایگان صدا بزنید، مثلاً https://api.github.com/users/<name>، و پنج فیلد از پاسخش را چاپ کنید.
  4. پاسخ همان وب‌سرویس را در فایل JSON ذخیره کنید.
  5. با matplotlib نمودار میله‌ای نمره‌های students.csv فصل قبل را رسم کنید.
  6. products.xml را بخوانید و فایل XML تازه‌ای بنویسید که فقط کارت‌های گرافیک را دارد.
  7. products.xml را به JSON و دوباره به XML تبدیل کنید و دو فایل XML را مقایسه کنید.
  8. خوراک RSS سایتی که دوست دارید را بخوانید و پنج عنوان آخرش را با تاریخشان چاپ کنید.
فصل قبل: CSV و JSON فصل بعد: ارزشیابی میان‌ترم