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

Professional Python: Web Scraping - Part 1

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

Web Scraping چیست؟

یکی از کارهایی که می توانیم با پایتون انجام بدهیم، web scraping است. فرآیند web scraping زمانی کاربرد دارد که شما به هر دلیل توانایی استفاده از API یک وب سایت را ندارید یا شاید آن وب سایت اصلا API ای نداشته باشد اما شما بخواهید به داده های آن دسترسی داشته باشید. با web scraping ما یک صفحه وب را به صورت عادی دریافت می کنیم (صفحه HTML) و سپس آن را تمیز کرده و داده های خودمان را از آنجا استخراج می کنیم.

فرض کنید شما صاحب یک وب سایت هستید و تلاش زیادی برای جمع آوری اطلاعات خود انجام داده اید. آیا عادلانه است که فردی به راحتی و با یک اسکریپت ساده تمام اطلاعات صفحات وب سایت شما را دریافت کند؟ پاسخ این سوال بسته به نوع داده های شما و نوع وب سایتتان دارد و نمی توانیم به این سادگی به آن پاسخ بدهیم. چرا؟ به دلیل اینکه صفحات وب برای استفاده عموم قرار داده شده اند و اینطور نیست که ما چیزی را هک کرده باشیم تا به این اطلاعات برسیم. تنها تفاوت web scraping با گشت و گذار در اینترنت این است که به جای استفاده از مرورگر، از کد استفاده می کنیم.

طبیعتا بسیاری از وب سایت ها دوست ندارند که شما صفحاتشان را scrape کنید بنابراین با روش های مختلفی از آن محافظت می کنند. یکی از این روش ها استفاده از فایل robots.txt است. به طور مثال اگر به وب سایت گیت ها برویم و آدرس github.com/robots.txt را باز کنیم توضیحاتی در این باره را می بینیم. در این صفحه به ما گفته شده است که:

# If you would like to crawl GitHub contact us via https://support.github.com/contact/

# We also provide an extensive API: https://developer.github.com/

User-agent: baidu

crawl-delay: 1

این متن ساده به ما می گوید که در صورت علاقه به crawl کردن این سایت باید از آنها اجازه بگیریم. مثال دیگر وب سایت airbnb است. برای مشاهده فایل robots.txt در آن باید به آدرس www.airbnb.ca/robots.txt مراجعه کنیم. در این آدرس فایل robots.txt برایمان باز می شود:

#    We thought you'd never make it!

#    We hope you feel right at home in this file...unless you're a disallowed subfolder.

#    And since you're here, read up on our culture and team: https://www.airbnb.com/careers/departments/engineering

#    There's even a bring your robot to work day.










User-agent: Googlebot

Allow: /calendar/ical/

Allow: /.well-known/amphtml/apikey.pub

Disallow: /account

Disallow: /alumni

Disallow: /associates/click

Disallow: /api/v1/trebuchet

Disallow: /api/v3

Disallow: /book/

Disallow: /calendar/

// بقیه کدها

در این فایل می بینید که disallow و allow را داریم که به ترتیب به معنای مجاز بودن و مجاز نبودن scraping هستند. ما می توانیم آدرس هایی را که در مقابل allow قرار دارند crawl یا scrape کنیم اما آدرس هایی که disallow شده اند مجاز به scrape شدن نیستند. در کد بالا قسمتی به نام User-agent وجود دارد که مشخص می کند این دستورات مربوط به چه نوع crawler ای می باشد و Googlebot هم crawler شرکت گوگل است.

برای مثال بعدی به وب سایت hacker news می رویم:

news.ycombinator.com/robots.txt

با باز کردن این آدرس نتیجه زیر را می گیریم:

User-Agent: *

Disallow: /x?

Disallow: /r?

Disallow: /vote?

Disallow: /reply?

Disallow: /submitted?

Disallow: /submitlink?

Disallow: /threads?

Crawl-delay: 30

در این قسمت User-Agent روی * گذاشته شده است که یعنی دستورات زیر برای تمام افراد و crawler ها صدق می کند. در این لیست چند مسیر ساده را می بینید که disallow شده اند بنابراین scrape کردن آن ها غیرمجاز است اما مسیرهای دیگری که در این قسمت ذکر نشده اند، مشکلی ندارند. تنها نکته باقی مانده Crawl-delay است که به ما می گوید بین هر بار scrape کردن باید ۳۰ ثانیه فاصله باشد تا سرورهای آن ها را تحت فشار قرار ندهیم.

نکته: شما در عمل می توانید مسیرهای غیر مجاز را نیز scrape کنید اما این کار از لحاظ اخلاقی و بعضا قانونی کار درستی نیست بنابراین فقط مسیرهایی را scrape کنید که اجازه scrape کردنشان را دارید.

Googlebot چیست؟

من در قسمت بالا خصوصیتی به نام user-agent را به شما نشان داده که Googlebot را در خود داشت اما Googlebot چیست؟ برای درک این موضوع باید با نحوه کار موتورهای جست و جو مانند Bing و Google آشنا شویم. گوگل سعی می کند تمام وب سایت های دنیا را پیدا کرده و scrape کند تا داده های آن ها را ذخیره کند. حالا زمانی که ما عبارتی را در گوگل جست و جو می کنیم، گوگل به دنبال این عبارت گشته و نتیجه را برای شما نمایش می دهد. Googlebot رباتی است که صفحات وب را scrape می کند یا به عبارت دقیق تر crawl می کند بنابراین Googlebot یک crawler یا خزنده وب است. البته تفاوت هایی جزئی بین crawling و scraping وجود دارد اما برای ما مهم نیست.

پروژه این قسمت: hacker news

یکی از وب سایت های مورد علاقه و بسیار مشهور در حوزه اخبار برنامه نویسی، hacker news است. اگر به زبان انگلیسی تسلط دارید به شما پیشنهاد می کنم حتما روزانه مطالب آن را دنبال کنید. سرعت پست اطلاعات در این وب سایت بسیار زیاد است تا حدی که در هر چند دقیقه و با چند بار refresh کردن صفحه شاهد مطالب کاملا جدیدی خواهید بود! hacker news در واقع لیستی از خبر های مربوط به حوزه تکنولوژی از وب سایت های دیگر است و با کلیک روی هر مقاله به وب سایت آن مقاله منتقل می شوید. همچنین کاربران می توانند مقالات پست شده توسط یکدیگر را upvote کنند (به آن رای بدهند) بنابراین مقالاتی که پرطرفدارتر باشند دارای upvote بیشتری خواهند بود. ما می خواهیم برای پروژه این قسمت، مقالات hacker news ای را که دارای upvote های زیادی هستند گرفته و آن ها را برای خودمان جداسازی کنیم. با این کار فقط مقالات پرطرفدار را مشاهده خواهیم کرد. هدف من در این پروژه فقط مقالاتی است که بالای ۱۰۰ لایک یا همان upvote داشته باشند.

آشنایی با Beautiful Soup

برای شروع پروژه یک پوشه جدید بسازید (من نام این پوشه را HN می گذارم) و داخل آن فایلی به نام scrape.py را ایجاد می کنم. در مرحله بعدی باید با کتابخانه Beautiful soup آشنا شویم؛ این کتابخانه یک HTML Parser است. یعنی چه؟ هنگامی که داده ها را از یک وب سایت scrape می کنیم طبیعتا فقط سورس آن را دریافت می کنیم که همان کد HTML است. کدهای HTML از نظر زبان پایتون یک سری متن و کاراکتر ساده هستند و معنای خاصی ندارند بنابراین به کتابخانه ای نیاز داریم که بتواند این کدهای HTML را تجزیه و تحلیل کند. آدرس کامل این پکیج در PYPI به شکل زیر است:

pypi.org/project/beautifulsoup4/

برای نصب این پکیج ابتدا دستور زیر را در ترمینال خود اجرا کنید:

pip3 install beautifulsoup4

با اجرای این دستور نتیجه ای شبیه به نتیجه زیر برایتان نمایش داده می شود:

Defaulting to user installation because normal site-packages is not writeable

Collecting beautifulsoup4

  Downloading beautifulsoup4-4.9.3-py3-none-any.whl (115 kB)

     |████████████████████████████████| 115 kB 234 kB/s

Collecting soupsieve>1.2

  Downloading soupsieve-2.1-py3-none-any.whl (32 kB)

Installing collected packages: soupsieve, beautifulsoup4

Successfully installed beautifulsoup4-4.9.3 soupsieve-2.1

در مرحله بعدی اگر پکیج requests را نصب نکرده اید باید آن را نیز با دستور زیر نصب کنید:

pip install requests

البته توجه داشته باشید که به دلیل محبوبیت پکیج requests بعضی از distro های لینوکس آن را به صورت پیش فرض روی سیستم شما نصب می کنند اما برای اطمینان بهتر است دستور بالا را یک بار در ترمینال خود اجرا کنید. حالا هر دو پکیج را در فایل scrape.py خود وارد می کنیم:

import requests

from bs4 import BeautifulSoup

اسکریپت نویسی برای دریافت داده های Hacker News

در این قسمت نوشتن اسکریپت خود را شروع می کنیم. همانطور که گفتم پکیج requests مسئولیت ارسال درخواست به سرورهای Hacker News را بر عهده دارد. به طور مثال به کد زیر توجه کنید:

import requests

from bs4 import BeautifulSoup




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




print(res.text)

من در این اسکریپت یک درخواست GET به وب سایت hacker news ارسال کرده ام و نتیجه اش را در متغیری به نام res ذخیره کرده ام. در مرحله بعد با استفاده از خصوصیت text می توانیم متن آن را بخوانیم (قبلا با requests کار کرده ایم بنابراین انتظار دارم با آن آشنا باشید). با اجرای کد بالا نتیجه ای به شکل زیر دریافت می کنیم (به دلیل طولانی بودن نتیجه من فقط بخشی از آن را قرار می دهم):

<html lang="en" op="news"><head><meta name="referrer" content="origin"><meta name="viewport" content="width=device-width, initial-scale=1.0"><link rel="stylesheet" type="text/css" href="news.css?cKJ6IAbSTfRG0X8OIelg">

        <link rel="shortcut icon" href="favicon.ico">

          <link rel="alternate" type="application/rss+xml" title="RSS" href="rss">

        <title>Hacker News</title></head><body><center><table id="hnmain" border="0" cellpadding="0" cellspacing="0" width="85%" bgcolor="#f6f6ef">

        <tr><td bgcolor="#ff6600"><table border="0" cellpadding="0" cellspacing="0" width="100%" style="padding:2px"><tr><td style="width:18px;padding-right:4px"><a href="https://news.ycombinator.com"><img src="y18.gif" width="18" height="18" style="border:1px white solid;"></a></td>

                  <td style="line-height:12pt; height:10px;"><span class="pagetop"><b class="hnname"><a href="news">Hacker News</a></b>

              <a href="newest">new</a> | <a href="front">past</a> | <a href="newcomments">comments</a> | <a href="ask">ask</a> | <a href="show">show</a> | <a href="jobs">jobs</a> | <a href="submit">submit</a>            </span></td><td style="text-align:right;padding-right:4px;"><span class="pagetop">

                              <a href="login?goto=news">login</a>

                          </span></td>

              </tr></table></td></tr>

<tr id="pagespace" title="" style="height:10px"></tr><tr><td><table border="0" cellpadding="0" cellspacing="0" class="itemlist">

              <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">102 points</span> by <a href="user?id=bkkaggle" class="hnuser">bkkaggle</a> <span class="age"><a href="item?id=25883791">2 hours ago</a></span> <span id="unv_25883791"></span> | <a href="hide?id=25883791&amp;goto=news">hide</a> | <a href="item?id=25883791">17&nbsp;comments</a>              </td></tr>

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

                <tr class='athing' id='25885108'>

      <td align="right" valign="top" class="title"><span class="rank">2.</span></td>      <td valign="top" class="votelinks"><center><a id='up_25885108' href='vote?id=25885108&amp;how=up&amp;goto=news'><div class='votearrow' title='upvote'></div></a></center></td><td class="title"><a href="https://vole.wtf/" class="storylink">Vole.wtf</a><span class="sitebit comhead"> (<a href="from?site=vole.wtf"><span class="sitestr">vole.wtf</span></a>)</span></td></tr><tr><td colspan="2"></td><td class="subtext">

        <span class="score" id="score_25885108">19 points</span> by <a href="user?id=fredley" class="hnuser">fredley</a> <span class="age"><a href="item?id=25885108">28 minutes ago</a></span> <span id="unv_25885108"></span> | <a href="hide?id=25885108&amp;goto=news">hide</a> | <a href="item?id=25885108">discuss</a>              </td></tr>

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

// بقیه کدها


همانطور که می بینید ما سورس کد HTML وب سایت hacker news را دریافت کرده ایم اما چطور می توانیم آن را از هم جدا کنیم تا داده های مورد نظرمان را دریافت کنیم؟ نتیجه بالا یک رشته طولانی است و برای پایتون معنای خاصی ندارد. اینجاست که beautifulsoup وارد می شود و به ما اجازه می دهد تا نتیجه را تحلیل کنیم.

برای استفاده از Beautiful soup می توانیم بدین شکل استفاده کنیم:

import requests

from bs4 import BeautifulSoup




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




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

ما داده های خومان (res.text) را به عنوان آرگومان اول به BeautifulSoup می دهیم و در آرگومان دوم نیز نوع تجزیه گر را مشخص می کنیم. پکیج Beautiful soup می تواند کدهای XML را نیز تجزیه کند ولی از آنجایی که کدهای ما HTML است حتما باید html.parser را به آرگومان دوم پاس بدهیم. برای اطلاعات بیشتر و مقایسه انواع تجزیه گر ها در Beautiful soup می توانید به صفحه زیر بروید:

www.crummy.com/software/BeautifulSoup/bs4/doc/#installing-a-parser

من کد بالا را ویرایش می کنم تا کد شیء soup را پرینت کنیم:

import requests

from bs4 import BeautifulSoup




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




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




print(soup)

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

<html lang="en" op="news"><head><meta content="origin" name="referrer"/><meta content="width=device-width, initial-scale=1.0" name="viewport"/><link href="news.css?cKJ6IAbSTfRG0X8OIelg" rel="stylesheet" type="text/css"/>

<link href="favicon.ico" rel="shortcut icon"/>

<link href="rss" rel="alternate" title="RSS" type="application/rss+xml"/>

<title>Hacker News</title></head><body><center><table bgcolor="#f6f6ef" border="0" cellpadding="0" cellspacing="0" id="hnmain" width="85%">

<tr><td bgcolor="#ff6600"><table border="0" cellpadding="0" cellspacing="0" style="padding:2px" width="100%"><tr><td style="width:18px;padding-right:4px"><a href="https://news.ycombinator.com"><img height="18" src="y18.gif" style="border:1px white solid;" width="18"/></a></td>

<td style="line-height:12pt; height:10px;"><span class="pagetop"><b class="hnname"><a href="news">Hacker News</a></b>

<a href="newest">new</a> | <a href="front">past</a> | <a href="newcomments">comments</a> | <a href="ask">ask</a> | <a href="show">show</a> | <a href="jobs">jobs</a> | <a href="submit">submit</a> </span></td><td style="text-align:right;padding-right:4px;"><span class="pagetop">

<a href="login?goto=news">login</a>

</span></td>

</tr></table></td></tr>

<tr id="pagespace" style="height:10px" title=""></tr><tr><td><table border="0" cellpadding="0" cellspacing="0" class="itemlist">

// بقیه کدها

همانطور که می بینید این بار کدهای HTML کمی تمیز تر شده و ساختار پیدا کرده اند. ما می توانیم با استفاده از این شیء soup قسمت های مختلف صفحه را دریافت کنیم. به طور مثال:

import requests

from bs4 import BeautifulSoup




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




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




print(soup.body.cotents)

با اجرای کد بالا محتویات درون تگ body و contents را مشاهده خواهیم کرد. البته به یاد داشته باشید که این محتوا در یک لیست برگردانده خواهد شد. حتی می توانیم با استفاده از متد find_all تمامی تگ های div را به شکل زیر پیدا کنیم:

import requests

from bs4 import BeautifulSoup




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




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




print(soup.find_all('div'))

نتیجه اجرای کد بالا بدین شکل است:

[<div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>, <div class="votearrow" title="upvote"></div>]

اگر دقت کنید متوجه خواهید شد که این نتیجه یک لیست است و هر کدام از div های پیدا شده در صفحه یکی از اعضای این لیست می باشند. در صورتی که از متد find_all استفاده نکرده و فقط از soup.div استفاده کنیم، تنها اولین تگ div را دریافت خواهیم کرد نه تمام تگ های div را! با این حساب باید به وب سایت hacker news رفته و ساختار HTML صفحات آن را بررسی کنیم تا بدانیم هر مطلب درون چه فیلدی قرار می گیرد. من ساختار خلاصه شده مطالب در این وب سایت را برایتان قرار می دهم:

<tbody>

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

  <tr></tr>

  <tr class="spacer"></tr>

</tbody>

تمام مطالب در hacker news درون یک تگ tbody قرار گرفته اند. درون این tbody ، تگ های tr مختلفی وجود دارند؛ اولین تگ tr کلاس athing را دارد که درون خود متن مقاله را دارد، دومین تگ tr جزئیات مربوط به مقاله را دارد (تعداد upvote ها، زمان انتشار، کامنت ها و غیره) و سومین تگ tr نیز کلاس spacer را دارد که بین مطالب فاصله می اندازد. این الگو ده ها بار درون tbody تکرار می شود و هر سه تگ tr مخصوص یک مطلب هستند. اگر بخواهم یک نمونه مطلب با ساختار کامل (نه خلاصه شده) را برایتان قرار دهیم، چنین چیزی را خواهیم داشت:

<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>

همانطور که می بینید ساختار کامل نیز به شکل بالا است و اطلاعات کامل تری به ما می دهد. شاید بپرسید من چطور این ساختار را به دست آورده ام؟ باید در مرورگر خود به سایت hacker news رفته و کلید f12 را بزنید تا developer tools برایتان باز شود. آنگاه می توانید سورس کد صفحه Hacker News را مطالعه کنید. در آنجا آنقدر باید کدها را مطالعه کنید تا الگوی تکرار شده در آن صفحه را پیدا کنید. ما می توانیم از این ساختار برای استخراج داده های مورد نظر خودمان استفاده کنیم. به طور مثال اگر بخواهیم تنها تعداد upvote های هر پست را دریافت کنیم، باید از متد select استفاده کنیم که یک CSS Selector را به عنوان آرگومان خود قبول می کند. ما می دانیم که تعداد upvote ها در یک تگ span با کلاس score ذخیره می شود (در ساختار بالا موجود است) بنابراین:

import requests

from bs4 import BeautifulSoup




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




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




print(soup.select('.score'))

علامت نقطه یک CSS Selector بوده و معنی کلاس را می دهد. یعنی هر عنصری که کلاس score را داشت استخراج می کنیم. با اجرای کد بالا نتیجه زیر را می گیریم:

[<span class="score" id="score_25885348">61 points</span>, <span class="score" id="score_25883791">137 points</span>, <span class="score" id="score_25885524">12 points</span>, <span class="score" id="score_25882688">121 points</span>, <span class="score" id="score_25881704">157 points</span>, <span class="score" id="score_25884159">30 points</span>, <span class="score" id="score_25881911">145 points</span>, <span class="score" id="score_25883853">35 points</span>, <span class="score" id="score_25883253">234 points</span>, <span class="score" id="score_25884338">20 points</span>, <span class="score" id="score_25862272">39 points</span>, <span class="score" id="score_25884762">15 points</span>, <span class="score" id="score_25870937">11 points</span>, <span class="score" id="score_25884781">10 points</span>, <span class="score" id="score_25870695">52 points</span>, <span class="score" id="score_25881740">39 points</span>, <span class="score" id="score_25882508">35 points</span>, <span class="score" id="score_25885244">78 points</span>, <span class="score" id="score_25882722">29 points</span>, <span class="score" id="score_25881126">92 points</span>, <span class="score" id="score_25883858">120 points</span>, <span class="score" id="score_25882021">172 points</span>, <span class="score" id="score_25885196">23 points</span>, <span class="score" id="score_25858457">68 points</span>, <span class="score" id="score_25885108">71 points</span>, <span class="score" id="score_25876767">826 points</span>, <span class="score" id="score_25869303">95 points</span>, <span class="score" id="score_25882319">23 points</span>, <span class="score" id="score_25884019">97 points</span>]

همانطور که می بینید تمام تگ های span دارای upvote ها در قالب یک لیست برایمان برگردانده شده اند. ما می توانیم از این متد برای هدف گرفتن دیگر سلکتورهای CSS نیز استفاده کنیم. به طور مثال:

print(soup.select('#score_25883791'))

با اجرای این دستور به دنبال عناصری می گردیم که id آن ها برابر با score_25883791 باشد. ما می توانیم از این منطق برای پیدا کردن لینک های مقالات استفاده کنیم. در hacker news لینک مقالات، عنوان مقالات نیز می باشد (عنوان در قالب یک تگ <a> پیاده شده است):

<a

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

  class="storylink"

  >Replicating GPT-2 at Home</a

>

همانطور که می بینید این لینک هم عنوان مقاله را دارد و هم لینک مربوط به آن را در خود جای داده است بنابراین می توانیم با استفاده از کلاس storylink این مقالات را دریافت کنیم:

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')

همانطور که می بینید من با استفاده از کلاس های  storylink و score موفق به دریافت عنوان مقالات و همینطور vote ها (تعداد لایک هایشان) شده ام. در مرحله بعدی باید این داده ها را فیلتر کنیم تا فقط شامل مقالاتی با بالاتر از ۱۰۰ لایک باشند. من برای انجام این کار یک تابع می نویسم. در ابتدا باید به جای HTML فقط متن عناوین را داشته باشیم:

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()

        hn.append(title)




    return hn







print(create_custom_hn(links, votes))

من یک لیست به نام hn را در این تابع ساخته ام و سپس یک حلقه for را نوشته ایم. در این حلقه idx (ایندکس) و item (مقدار واقعی) را دریافت می کنیم (قبلا با enumerate آشنا شده اید) و سپس با استفاده از متد getText که یکی از متدهای Beautiful soup می باشد، متن تمام لینک ها را دریافت کرده و به لیست hn اضافه کرده ایم. من برای تست این تابع را صدا زده و نتیجه اش را چاپ کرده ام. این نتیجه به شکل زیر خواهد بود:

['Pip has dropped support for Python 2', 'Whitehouse.gov Chooses WordPress, Again', 'Jeff Bezos and Amazon do not want their workers voting by mail on unionization', 'I bought 200 Raspberry Pi Model B’s and I’m going to fix them', 'Why Working from Home Will Stick [pdf]', 'Making Win32 APIs More Accessible to More Languages', 'Show HN: Full text search Project Gutenberg (60m paragraphs)', 'Forever chemicals are widespread in U.S. drinking water', 'How long does a bottle of wine last after it is opened?', 'TLB and Pagewalk Coherence in x86 Processors (2015)', 'Show HN: Filmulator – a streamlined, open-source raw photo editor', 'Software engineering topics I changed my mind on', 'Site.js: Small Web construction set', 'Tree – a lib for working with nested data structures, open-sourced by deepmind', 'Game Design Perspective: Stardew Valley (2020)', 'Incompatible Timesharing System', 'When Historical Fiction Is a Crime (2020)', 'Curve.fi is hiring Python (Vyper) dev and QA (py.test). Math knowledge is and+', 'DrScheme in Space', 'Replicating GPT-2 at Home', 'MkLinux', 'Visual Sentence Composer for Japanese', 'Emails a browser extension developer gets from scammers', 'Uber Lays off Postmates Founder/CEO, 180 others', 'Flipper Zero Manufacturing and Shipping Plan', "2 times 3 can sometimes equal 7 with Android's Neural Network API", 'Malware found on laptops given out by government', 'VisualAge for Java 1.0 – Let the Future Begin (1997)', 'Google has turned off access to sync features for Chromium', 'Cryptocurreny crime is way ahead of regulators and law enforcement']

همانطور که می بینید حالا دیگر خبری از HTML نبوده و فقط عناوین را در یک لیست داریم. البته حالا به مشکل جدیدی برخورد کرده ایم؛ ما متن مقالات را داریم اما لینک را از دست داده ایم! برای حل این مشکل کد را به شکل زیر ویرایش می کنیم:

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)

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




    return hn







print(create_custom_hn(links, votes))

من در اینجا متغیری به نام href را نیز اضافه کرده ام که قرار است حاوی لینک ها باشد. باز هم با استفاده از ایندکس در حلقه (idx) تک تک تگ های <a> را دریافت کرده ایم و سپس با متد get می توانیم خصوصیات این لینک را بگیریم. من خصوصیت href را می خواهم بنابراین href را به آن پاس داده ام و برای اطمینان،‌ آرگومان دوم را روی None گذاشته ام. آیا می دانید آرگومان دوم چه کار می کند؟ وظیفه آرگومان دوم ثبت مقدار پیش فرض است تا اگر احیانا یکی از تگ ها دارای href نبود، به جایش یک مقدار پیش فرض (None) داشته باشیم. در نهایت برای اینکه بتوانیم مقالات و لینک هایشان را به هم متصل کنیم از یک dictionary استفاده کرده ایم. با این حساب محصول نهایی ما لیستی از دیکشنری های مختلف خواهد بود.

حالا می خواهم از شما سوالی بپرسم؛ آیا روش زیر برای دریافت تعداد upvote ها صحیح است؟

for idx, item in enumerate(links):

    title = links[idx].getText()

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

    votes = votes[idx].getText()

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

اگر این کد را اجرا کنید ممکن است به خطا برخورد کنید. چرا؟ به دلیل اینکه برخی اوقات مقاله ای تازه ثبت شده است و هنوز هیچ upvote ای ندارد و ما سعی می کنیم مقداری که وجود ندارد را دریافت کنیم و خطا می گیریم. همچنین در روش بالا votes به شکل رشته دریافت می شود بنابراین باید آن را به عدد تبدیل کنیم. از طرف دیگر متن درون <span> به شکل points 100 است بنابراین باید قسمت points را نیز حذف کنیم تا فقط عدد 100 را بگیریم. من ابتدا دو مشکل رشته ای بودن و حضور متن 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', ''))

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




    return hn







print(create_custom_hn(links, votes))

تابع int باعث عددی شدن مقدار شده و همچنین تابع replace به دنبال رشته ای می گردد که با اسپیس و سپس points شروع شود، سپس آن رشته را گرفته و با یک رشته خالی جایگزین می کند. به زبان ساده قسمت points را حذف کرده ایم تا فقط عدد را دریافت کنیم اما هنوز مشکلات بعدی خود را حل نکرده ایم! من قسمت ابتدایی کار را برای شما انجام دادم و حالا از شما می خواهم که روی این موضوع تمرکز کرده و خودتان مشکلات کد بالا را حل کنید. در جلسه بعدی این کار را به همراه هم انجام خواهیم داد.

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

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

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