Python حرفه‌ای: آشنایی با Web Scraping - بخش ۲

Professional Python: Web Scraping - Part 2

25 اسفند 1399
درسنامه درس 31 از سری پایتون حرفه‌ای
Python حرفه ای: آشنایی با Web Scraping - بخش ۲ (قسمت 31)

مشکل عدم وجود score در بعضی از پست ها

در جلسه قبل در هنگام scrape کردن وب سایت hacker news به چند مشکل برخورد کردیم. اولین مشکل این بود که برخی اوقات مقاله ای تازه ثبت شده است و هنوز هیچ upvote ای ندارد بنابراین ما در حال دریافت score ای هستیم که اصلا وجود ندارد و طبیعتا خطا می گیریم. مشکل دوم این بود که votes (تعداد رای ها) به شکل رشته دریافت می شود اما ما می خواهیم آن را به صورت عدد دریافت کنیم. مشکل سوم نیز این بود که متن درون <span> به شکل points 100 است یعنی کلمه points را نیز در کنار خود دارد بنابراین باید این قسمت را حذف کنیم تا فقط عدد 100 را بگیریم.

برای حل مشکلات مربوط به رشته ای بودن votes و همچنین حذف رشته points از کد زیر استفاده می کنیم:

import requests

from bs4 import BeautifulSoup




res = requests.get('https://news.ycombinator.com/')




soup = BeautifulSoup(res.text, 'html.parser')




links = soup.select('.storylink')

votes = soup.select('.score')







def create_custom_hn(links, votes):

    hn = []




    for idx, item in enumerate(links):

        title = links[idx].getText()

        href = links[idx].get('href', None)

        votes = int(votes[idx].getText().replace(' points', ''))

        print(votes)

        hn.append({'title': title, 'link': href})




    return hn







create_custom_hn(links, votes)

# print(create_custom_hn(links, votes))

اما این کد مشکل ما را حل نمی کند. اولا که این کد بسیار شلوغ و ناخوانا است، ثانیا اگر به span ای برخورد کنیم که دارای upvote نباشد باز هم کد ما خطا گرفته و متوقف می شود. برای حل این مشکل باید به ساختار مقالات در وب سایت hacker news نگاهی دوباره بیندازیم:

<tbody>

  <tr class="athing" id="25883791">

    <td align="right" valign="top" class="title">

      <span class="rank">1.</span>

    </td>

    <td valign="top" class="votelinks">

      <center>

        <a id="up_25883791" href="vote?id=25883791&amp;how=up&amp;goto=news"

          ><div class="votearrow" title="upvote"></div

        ></a>

      </center>

    </td>

    <td class="title">

      <a

        href="https://bkkaggle.github.io/blog/algpt2/2020/07/17/ALGPT2-part-2.html"

        class="storylink"

        >Replicating GPT-2 at Home</a

      ><span class="sitebit comhead">

        (<a href="from?site=bkkaggle.github.io"

          ><span class="sitestr">bkkaggle.github.io</span></a

        >)</span

      >

    </td>

  </tr>




  <tr>

    <td colspan="2"></td>

    <td class="subtext">

      <span class="score" id="score_25883791">115 points</span> by

      <a href="user?id=bkkaggle" class="hnuser">bkkaggle</a>

      <span class="age"><a href="item?id=25883791">3 hours ago</a></span>

      <span id="unv_25883791"></span> |

      <a href="hide?id=25883791&amp;goto=news">hide</a> |

      <a href="item?id=25883791">21&nbsp;comments</a>

    </td>

  </tr>




  <tr class="spacer" style="height: 5px"></tr>




// تکرار الگو به تعداد زیاد




</tbody>

احتمالا شما نیز متوجه شده اید که تعداد upvote های هر مقاله درون یک td با کلاس subtext قرار دارد و حتی اگر مقاله ای رای نداشته باشد، باز هم قسمت subtext وجود خواهد داشت (قسمت ثابت و جزئی از ساختار صفحه) بنابراین می توانیم آن را هدف بگیریم:

import requests

from bs4 import BeautifulSoup




res = requests.get('https://news.ycombinator.com/')




soup = BeautifulSoup(res.text, 'html.parser')




links = soup.select('.storylink')

subtext = soup.select('.subtext')







def create_custom_hn(links, subtext):

    hn = []




    for idx, item in enumerate(links):

        title = links[idx].getText()

        href = links[idx].get('href', None)

        vote = subtext[idx].select('.score')

        if len(vote):

            points = int(vote[0].getText().replace(' points', ''))

            print(points)

            hn.append({'title': title, 'link': href})




    return hn







create_custom_hn(links, subtext)

# print(create_custom_hn(links, subtext))

همانطور که می بینید من نام چند متغیر را در این قسمت تغییر داده ام. در ابتدا به جای votes حالا subtext را داریم تا به مشکل عدم وجود آن برخورد نکنیم اما در حلقه خود از متد select استفاده کرده ایم تا عنصری که درون آن کلاس score دارد را دریافت کنیم. با انجام این کار دیگر score را به صورت مستقیم دریافت نمی کنیم و طبیعتا به خطا نیز برخورد نمی کنیم. من با اجرای کد بالا نتیجه زیر را می گیرم:

63

414

102

33

25

14

12

126

72

22

18

47

62

11

19

65

10

249

7

4

10

8

10

5

173

285

33

206

5

بنابراین تمام vote ها را دریافت می کنیم و خطایی نیز نداریم. تنها کاری که باقی مانده است اضافه کردن این امتیازها به دیکشنری نهایی ما است:

def create_custom_hn(links, subtext):

    hn = []




    for idx, item in enumerate(links):

        title = links[idx].getText()

        href = links[idx].get('href', None)

        vote = subtext[idx].select('.score')

        if len(vote):

            points = int(vote[0].getText().replace(' points', ''))

            print(points)

            hn.append({'title': title, 'link': href, 'votes': points})




    return hn

در ضمن شاید متوجه دلیل استفاده از enumerate نشده باشید بنابراین دوست دارم توجه شما را به آن جلب کنم. برای اینکه بتوانیم هم به لینک مورد نظر و هم به subtext دسترسی داشته باشیم باید ایندکس گردش روی links را به دست بیاوریم. استفاده از enumerate راهی آسان برای به دست آوردن ایندکس بود. حالا اگر اسکریپت خودمان را اجرا کنیم نتیجه را به شکل کامل دریافت می کنیم اما بهتر است نتایج را فیلتر کنیم تا فقط مقالاتی را ببینیم که بالای ۱۰۰ رای دارند. چطور؟ با یک شرط ساده می توانیم فقط مقالات با رای بیشتر از ۱۰۰ را append کنیم:

import requests

from bs4 import BeautifulSoup




res = requests.get('https://news.ycombinator.com/')




soup = BeautifulSoup(res.text, 'html.parser')




links = soup.select('.storylink')

subtext = soup.select('.subtext')







def create_custom_hn(links, subtext):

    hn = []




    for idx, item in enumerate(links):

        title = links[idx].getText()

        href = links[idx].get('href', None)

        vote = subtext[idx].select('.score')

        if len(vote):

            points = int(vote[0].getText().replace(' points', ''))

            if points > 99:

                hn.append({'title': title, 'link': href, 'votes': points})




    return hn







print(create_custom_hn(links, subtext))

با اجرای اسکریپت بالا نتیجه ای شبیه به نتیجه زیر را دریافت می کنیم:

[{'title': 'Deskreen – Turn any device with a web browser to a second computer screen', 'link': 'https://github.com/pavlobu/deskreen', 'votes': 425}, {'title': 'Solar is now ‘cheapest electricity in history’, confirms IEA', 'link': 'https://www.weforum.org/agenda/2020/10/solar-cheap-energy-coal-gas-renewables-climate-change-environment-sustainability', 'votes': 137}, {'title': 'All 104 amendments to the Constitution of India as Git commits', 'link': 'https://github.com/prince-mishra/the-constitution-of-india', 'votes': 135}, {'title': 'Lack of sleep, stress can lead to symptoms resembling concussion', 'link': 'https://news.osu.edu/lack-of-sleep-stress-can-lead-to-symptoms-resembling-concussion/', 'votes': 254}, {'title': 'Show HN: Full text search Project Gutenberg (60m paragraphs)', 'link': 'https://gutensearch.com/', 'votes': 177}, {'title': 'SoftBank’s plan to sell Arm to NVIDIA is hitting antitrust wall around the world', 'link': 'https://asia.nikkei.com/Business/Technology/SoftBank-s-plan-to-sell-Arm-to-NVIDIA-is-hitting-antitrust-wall-around-the-world', 'votes': 289}, {'title': 'The people the suburbs were built for are gone', 'link': 'https://www.vice.com/en/article/y3gx5b/the-people-the-suburbs-were-built-for-are-gone', 'votes': 211}]

مشکل این نتیجه، این است که به شدت بهم ریخته و همه چیز درهم چاپ شده است. برای بهتر کردن نحوه نمایش داده ها می توانیم از ماژول pprint (مخفف pretty print - به معنی پرینت زیبا) استفاده کنیم:

import requests

from bs4 import BeautifulSoup

import pprint




res = requests.get('https://news.ycombinator.com/')




soup = BeautifulSoup(res.text, 'html.parser')




links = soup.select('.storylink')

subtext = soup.select('.subtext')







def create_custom_hn(links, subtext):

    hn = []




    for idx, item in enumerate(links):

        title = links[idx].getText()

        href = links[idx].get('href', None)

        vote = subtext[idx].select('.score')

        if len(vote):

            points = int(vote[0].getText().replace(' points', ''))

            if points > 99:

                hn.append({'title': title, 'link': href, 'votes': points})




    return hn







pprint.pprint(create_custom_hn(links, subtext))

حالا که به جای print از دستور pprint استفاده کرده ایم نتیجه را به شکل زیر مشاهده می کنیم:

[{'link': 'https://github.com/pavlobu/deskreen',

  'title': 'Deskreen – Turn any device with a web browser to a second computer '

           'screen',

  'votes': 428},

 {'link': 'https://www.weforum.org/agenda/2020/10/solar-cheap-energy-coal-gas-renewables-climate-change-environment-sustainability',

  'title': 'Solar is now ‘cheapest electricity in history’, confirms IEA',

  'votes': 139},

 {'link': 'https://github.com/prince-mishra/the-constitution-of-india',

  'title': 'All 104 amendments to the Constitution of India as Git commits',

  'votes': 135},

 {'link': 'https://news.osu.edu/lack-of-sleep-stress-can-lead-to-symptoms-resembling-concussion/',

  'title': 'Lack of sleep, stress can lead to symptoms resembling concussion',

  'votes': 254},

 {'link': 'https://gutensearch.com/',

  'title': 'Show HN: Full text search Project Gutenberg (60m paragraphs)',

  'votes': 177},

 {'link': 'https://asia.nikkei.com/Business/Technology/SoftBank-s-plan-to-sell-Arm-to-NVIDIA-is-hitting-antitrust-wall-around-the-world',

  'title': 'SoftBank’s plan to sell Arm to NVIDIA is hitting antitrust wall '

           'around the world',

  'votes': 289},

 {'link': 'https://www.vice.com/en/article/y3gx5b/the-people-the-suburbs-were-built-for-are-gone',

  'title': 'The people the suburbs were built for are gone',

  'votes': 212}]

همانطور که می بینید ظاهر این خروجی بسیار تمیزتر و خوانا تر است. در ضمن همانطور که مشاهده می کنید تمام مقالات دریافت شده بالای ۹۹ رای دارند بنابراین کدها به خوبی کار خودشان را انجام داده اند اما بهتر است این مقالات را مرتب کنیم به طوری که مقالات با رای بیشتر در بالاترین قسمت آن قرار داشته باشند (مرتب سازی یا sorting). من پیشنهاد می کنم حتما خودتان روی این مسئله فکر کرده و آن را حل کنید، سپس به پاسخ من نگاه کرده و آن ها را با هم مقایسه کنید. شاید راه شما از راه من بهتر باشد!

مرتب سازی مقالات بر اساس vote

برای مرتب کردن مقالات بر اساس تعداد vote های موجود بهتر است یک تابع دیگر را تعریف کنیم تا تابع فعلی مان بیش از حد شلوغ نشود. در پایتون تابعی به نام sorted وجود دارد که کار مرتب سازی را انجام می دهد اما مقادیر ما درون دیکشنری ها قرار دارند و این تابع نمی تواند این اشیاء را مرتب کند مگر اینکه دقیقا مشخص کنیم فرآیند مرتب سازی بر چه اساسی صورت بگیرد. یعنی چه؟ شیء ما سه فیلد متفاوت دارد: عنوان مقاله، لینک مقاله، تعداد vote ها. تابع sorted نمی داند باید بر اساس کدام فیلد آن ها را مرتب کند. برای حل این مشکل می توانیم به عنوان پارامتر دوم (key) یک تابع lambda را به آن پاس بدهیم:

import requests

from bs4 import BeautifulSoup

import pprint




res = requests.get('https://news.ycombinator.com/')




soup = BeautifulSoup(res.text, 'html.parser')




links = soup.select('.storylink')

subtext = soup.select('.subtext')







def sort_stories_by_votes(hnlist):

    return sorted(hnlist, key=lambda k: k['votes'], reverse=True)







def create_custom_hn(links, subtext):

    hn = []




    for idx, item in enumerate(links):

        title = links[idx].getText()

        href = links[idx].get('href', None)

        vote = subtext[idx].select('.score')

        if len(vote):

            points = int(vote[0].getText().replace(' points', ''))

            if points > 99:

                hn.append({'title': title, 'link': href, 'votes': points})




    return sort_stories_by_votes(hn)







pprint.pprint(create_custom_hn(links, subtext))

همینطور که در کد بالا مشاهده می کنید من تابع جدیدی به نام sort_stories_by_votes را تعریف کرده ام و در آن تابع sorted را صدا زده ام؛ آرگومان اول آن، داده های ما است، آرگومان دوم یک تابع lambda است که مشخص می کند دقیقا از کدام کلید دیکشنری برای مرتب سازی استفاده شود و آرگومان سوم برای برعکس کردن نتایج است. چرا؟ به دلیل اینکه تابع sorted به صورت پیش فرض مرتب سازی را «صعودی» انجام می دهد یعنی کمترین مقدار در ابتدا و بیشتری مقدار در انتها باشد اما ما می خواهیم بالاترین مقدار (مقاله با بیشتری تعداد vote) در ابتدای لیست باشد.

با اجرای اسکریپت بالا نتیجه زیر را دریافت می کنیم:

[{'link': 'https://github.com/pavlobu/deskreen',

  'title': 'Deskreen – Turn any device with a web browser to a second computer '

           'screen',

  'votes': 642},

 {'link': 'https://www.theguardian.com/technology/2021/jan/24/whatsapp-loses-millions-of-users-after-terms-update',

  'title': 'WhatsApp loses millions of users after terms update',

  'votes': 259},

 {'link': 'https://infosec-handbook.eu/news/2020-12-05-leaving-the-fediverse/',

  'title': 'Our experience with the Fediverse, and why we left',

  'votes': 174},

 {'link': 'https://github.com/cytopia/ffscreencast',

  'title': 'ffscreencast – a screencast CLI-tool with video overlay and '

           'multimonitor support',

  'votes': 161},

 {'link': 'https://delta.chat/en/',

  'title': 'Delta Chat – decentralized chat via email',

  'votes': 157},

 {'link': 'https://blog.racket-lang.org/2021/01/racket-status.html',

  'title': 'Racket Compiler and Runtime Status',

  'votes': 151},

 {'link': 'http://www.mit.edu/afs.new/sipb/user/marthag/postscript/burritos',

  'title': 'Ordering burritos from my SPARC (1992)',

  'votes': 148},

 {'link': 'https://catonmat.net/videos/the-computer-revolution-hasnt-happened-yet',

  'title': 'The computer revolution hasn’t happened yet (1997)',

  'votes': 147},

 {'link': 'https://skamille.medium.com/make-boring-plans-9438ce5cb053',

  'title': 'Make Boring Plans',

  'votes': 137},

 {'link': 'https://julian.digital/2020/08/06/proof-of-x/',

  'title': 'Proof of X',

  'votes': 126},

 {'link': 'https://arstechnica.com/cars/2021/01/waymo-ceo-tesla-is-not-a-competitor-at-all/',

  'title': 'Waymo CEO dismisses Tesla self-driving plan: “This is not how it '

           'works”',

  'votes': 114},

 {'link': 'https://covidvaxcount.live/',

  'title': '18.7 Million Americans Vaccinated',

  'votes': 111},

 {'link': 'https://www.smashingmagazine.com/2021/01/smashingmag-performance-case-study/',

  'title': "How we improved our website's performance",

  'votes': 103}]

همانطور که می بینید تمام مقالات بالاتر از ۹۹ رای دارند و از بیشترین رای به کمترین رای مرتب شده اند.

scrape داده ها در صفحه دوم

ما تا این بخش به خوبی توانسته ایم داده هایمان را scrape کنیم اما این scrape کردن فقط برای صفحه اول بوده است و به صفحات دیگر دسترسی نداریم. چطور می توانیم داده های صفحه دوم را نیز در نتایج داشته باشیم؟ اگر به وب سایت hacker news برویم، URL زیر برایتان باز می شود:

news.ycombinator.com

این صفحه اولین صفحه hacker news است که حدود ۳۰ مقاله اول را به شما نمایش می دهد.  اگر بخواهیم مقالات بیشتری را ببینیم باید به انتهای صفحه اسکرول کرده و روی گزینه more کلیک کنیم. با انجام این کار URL به شکل زیر تغییر می کند:

news.ycombinator.com/news?p=2

p=2 به معنی page = 2 می باشد که یعنی در صفحه دوم هستیم بنابراین متوجه می شویم که این وب سایت از query string ها استفاده می کند. ما می توانیم از همین مفهوم ساده استفاده کرده و به صفحات دیگر نیز دسترسی داشته باشیم.

import requests

from bs4 import BeautifulSoup

import pprint




res = requests.get('https://news.ycombinator.com/news')

res2 = requests.get('https://news.ycombinator.com/news?p=2')

soup = BeautifulSoup(res.text, 'html.parser')

soup2 = BeautifulSoup(res2.text, 'html.parser')




links = soup.select('.storylink')

subtext = soup.select('.subtext')

links2 = soup2.select('.storylink')

subtext2 = soup2.select('.subtext')




mega_links = links + links2

mega_subtext = subtext + subtext2







def sort_stories_by_votes(hnlist):

    return sorted(hnlist, key=lambda k: k['votes'], reverse=True)







def create_custom_hn(links, subtext):

    hn = []

    for idx, item in enumerate(links):

        title = item.getText()

        href = item.get('href', None)

        vote = subtext[idx].select('.score')

        if len(vote):

            points = int(vote[0].getText().replace(' points', ''))

            if points > 99:

                hn.append({'title': title, 'link': href, 'votes': points})

    return sort_stories_by_votes(hn)







pprint.pprint(create_custom_hn(mega_links, mega_subtext))

من در کد بالا یک درخواست دیگر را به صفحه دوم hacker news ارسال کرده ام و داده های آن را استخراج کرده ام. حالا از همان منطق و اسکریپت قبلی برای گردش بین این مقالات استفاده می کنیم. تنها تفاوت این است که ابتدا باید links را با links2 جمع بزنیم! یعنی چه؟ توجه داشته باشید که این مقادیر (links2 و links و subtext و subtext2 همگی رشته های HTML هستند بنابراین می توانیم با استفاده از اپراتور + آن ها را با هم ادغام کنیم. بقیه کدها نیز دقیقا مانند قبل است و چیزی را تغییر نداده ایم. با اجرای اسکریپت بالا نتیجه زیر را دریافت می کنیم:

[{'link': 'https://github.com/pavlobu/deskreen',

  'title': 'Deskreen – Turn any device with a web browser to a second computer '

           'screen',

  'votes': 647},

 {'link': 'https://www.theguardian.com/technology/2021/jan/24/whatsapp-loses-millions-of-users-after-terms-update',

  'title': 'WhatsApp loses millions of users after terms update',

  'votes': 292},

 {'link': 'https://github.com/prince-mishra/the-constitution-of-india',

  'title': 'All 104 amendments to the Constitution of India as Git commits',

  'votes': 242},

 {'link': 'https://arstechnica.com/tech-policy/2021/01/military-intelligence-buys-location-data-instead-of-getting-warrants-memo-shows/',

  'title': 'Military intelligence buys location data instead of getting '

           'warrants',

  'votes': 207},

 {'link': 'https://infosec-handbook.eu/news/2020-12-05-leaving-the-fediverse/',

  'title': 'Our experience with the Fediverse, and why we left',

  'votes': 177},

 {'link': 'https://wiki.csswg.org/ideas/mistakes',

  'title': 'Incomplete List of Mistakes in the Design of CSS',

  'votes': 167},

 {'link': 'https://github.com/cytopia/ffscreencast',

  'title': 'ffscreencast – a screencast CLI-tool with video overlay and '

           'multimonitor support',

  'votes': 166},

 {'link': 'https://www.theguardian.com/world/2021/jan/24/as-birth-rates-fall-animals-prowl-in-our-abandoned-ghost-villages',

  'title': 'Ghost cities and abandoned areas with a declining population',

  'votes': 163},

 {'link': 'https://sokyokuban.com/',

  'title': 'Show HN: Sokyokuban, a puzzle game in a non-Euclidian world',

  'votes': 160},

 {'link': 'https://delta.chat/en/',

  'title': 'Delta Chat – decentralized chat via email',

  'votes': 159},

 {'link': 'http://www.mit.edu/afs.new/sipb/user/marthag/postscript/burritos',

  'title': 'Ordering burritos from my SPARC (1992)',

  'votes': 155},

 {'link': 'https://blog.racket-lang.org/2021/01/racket-status.html',

  'title': 'Racket Compiler and Runtime Status',

  'votes': 154},

 {'link': 'https://catonmat.net/videos/the-computer-revolution-hasnt-happened-yet',

  'title': 'The computer revolution hasn’t happened yet (1997)',

  'votes': 153},

 {'link': 'https://www.linkedin.com/pulse/why-i-wouldnt-invest-open-source-companies-even-though-wolfram-hempel',

  'title': "I wouldn't invest in open-source companies, even though I ran one",

  'votes': 145},

 {'link': 'https://skamille.medium.com/make-boring-plans-9438ce5cb053',

  'title': 'Make Boring Plans',

  'votes': 139},

 {'link': 'https://julian.digital/2020/08/06/proof-of-x/',

  'title': 'Proof of X',

  'votes': 130},

 {'link': 'https://www.bbc.co.uk/news/science-environment-55775977',

  'title': 'SpaceX: World record number of satellites launched',

  'votes': 125},

 {'link': 'https://arstechnica.com/cars/2021/01/waymo-ceo-tesla-is-not-a-competitor-at-all/',

  'title': 'Waymo CEO dismisses Tesla self-driving plan: “This is not how it '

           'works”',

  'votes': 124},

 {'link': 'https://covidvaxcount.live/',

  'title': '18.7 Million Americans Vaccinated',

  'votes': 124},

 {'link': 'https://www.sfchronicle.com/bayarea/heatherknight/article/San-Francisco-is-one-of-California-s-most-15891810.php',

  'title': 'San Francisco is one of CA’s most conservative cities – when it '

           'comes to housing',

  'votes': 105},

 {'link': 'https://www.smashingmagazine.com/2021/01/smashingmag-performance-case-study/',

  'title': "How we improved our website's performance",

  'votes': 104}]

شاید در نظر اول تفاوت زیادی بین این نتیجه و نتایج قبلی نباشد اما اگر خوب دقت کنید متوجه حضور مقالات جدیدی می شوید که در نتیجه های قبلی وجود نداشتند.

قدم بعدی؟

ما در این جلسات با Beautiful Soup آشنا شدیم و مباحث ساده و پایه ای آن را می دانیم اما روشی که برای شما توضیح دادم فقط برای وب سایت های ساده ای مانند hacker news کارایی دارد. وب سایت های بزرگ تر که بیشتر به جاوا اسکریپت وابسته هستند یا وب سایت های شرکت های بزرگ معمولا به شما اجازه scrape کردن را نمی دهند بنابراین استفاده از Beautiful Soup کارایی ندارد. در این حالت باید از فریم ورکی به نام scrapy استفاده کنید. توجه کنید که من از عبارت فریم ورک (framework) استفاده کرده ام نه کتابخانه، چرا که scrapy بسیار کامل تر از کتابخانه ای مانند Beautiful Soup است. طبیعتا یادگیری scrapy نیز زمان بر است و هنگامی توصیه می شود که بخواهید به صورت حرفه ای وارد scraping شوید.

از جلسه آینده وارد مبحث web development یا توسعه وب می شویم تا یاد بگیریم چطور می توان با استفاده از زبان پایتون یک وب سایت ساده بسازیم! ما در این مورد از فریم ورک Flask استفاده خواهیم کرد که یک فریم ورک سبک برای ساخت صفحات وب می باشد.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری پایتون حرفه‌ای توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.