<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alina Shah</title>
    <description>The latest articles on DEV Community by Alina Shah (@alinashah).</description>
    <link>https://dev.to/alinashah</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4139861%2Fad406f4f-1a76-4259-9ba8-42437ccdbfc3.jpg</url>
      <title>DEV Community: Alina Shah</title>
      <link>https://dev.to/alinashah</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alinashah"/>
    <language>en</language>
    <item>
      <title>Choosing an API gateway is about more than routing requests. Security, scalability, rate limiting, and failover all matter in production. This guide compares 6 options to help developers choose the right fit.</title>
      <dc:creator>Alina Shah</dc:creator>
      <pubDate>Sat, 31 Oct 2026 15:45:15 +0000</pubDate>
      <link>https://dev.to/alinashah/choosing-an-api-gateway-is-about-more-than-routing-requests-security-scalability-rate-limiting-13hc</link>
      <guid>https://dev.to/alinashah/choosing-an-api-gateway-is-about-more-than-routing-requests-security-scalability-rate-limiting-13hc</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/devstackhub/6-best-api-gateways-for-developers-breaking-their-routing-logic-4a99" class="crayons-story__hidden-navigation-link"&gt;6 Best API Gateways for Developers Breaking Their Routing Logic&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/devstackhub" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4081963%2F9d10c5fa-c76e-4c9d-88af-e3f2fced5a62.jpg" alt="devstackhub profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/devstackhub" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Dev Stack Hub
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Dev Stack Hub
                
                
              
              &lt;div id="story-author-preview-content-4953228" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/devstackhub" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4081963%2F9d10c5fa-c76e-4c9d-88af-e3f2fced5a62.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Dev Stack Hub&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/devstackhub/6-best-api-gateways-for-developers-breaking-their-routing-logic-4a99" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 31&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/devstackhub/6-best-api-gateways-for-developers-breaking-their-routing-logic-4a99" id="article-link-4953228"&gt;
          6 Best API Gateways for Developers Breaking Their Routing Logic
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/discuss"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;discuss&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/api"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;api&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/backend"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;backend&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/devstackhub/6-best-api-gateways-for-developers-breaking-their-routing-logic-4a99" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;27&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/devstackhub/6-best-api-gateways-for-developers-breaking-their-routing-logic-4a99#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              7&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            19 min read
          &lt;/small&gt;
          
          &lt;div class="only-sidebar-menu-item trusted-visible-block print-hidden align-center"&gt;
                
                    
                
          &lt;/div&gt;

        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>api</category>
      <category>architecture</category>
      <category>backend</category>
      <category>devops</category>
    </item>
    <item>
      <title>Struggling with JavaScript-heavy websites? Learn how to scrape dynamic content using Playwright and Python, handle infinite scrolling, capture API responses, and build reliable web scrapers. 🚀</title>
      <dc:creator>Alina Shah</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:18:14 +0000</pubDate>
      <link>https://dev.to/alinashah/struggling-with-javascript-heavy-websites-learn-how-to-scrape-dynamic-content-using-playwright-and-5agi</link>
      <guid>https://dev.to/alinashah/struggling-with-javascript-heavy-websites-learn-how-to-scrape-dynamic-content-using-playwright-and-5agi</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/alinashah/scrape-javascript-heavy-sites-with-playwright-in-python-developer-guide-49mh" class="crayons-story__hidden-navigation-link"&gt;Scrape JavaScript-Heavy Sites with Playwright in Python (Developer Guide)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/alinashah" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4139861%2Fad406f4f-1a76-4259-9ba8-42437ccdbfc3.jpg" alt="alinashah profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/alinashah" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Alina Shah
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Alina Shah
                
                
              
              &lt;div id="story-author-preview-content-4953037" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/alinashah" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4139861%2Fad406f4f-1a76-4259-9ba8-42437ccdbfc3.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Alina Shah&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/alinashah/scrape-javascript-heavy-sites-with-playwright-in-python-developer-guide-49mh" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 31&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/alinashah/scrape-javascript-heavy-sites-with-playwright-in-python-developer-guide-49mh" id="article-link-4953037"&gt;
          Scrape JavaScript-Heavy Sites with Playwright in Python (Developer Guide)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/discuss"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;discuss&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/python"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;python&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/playwright"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;playwright&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/alinashah/scrape-javascript-heavy-sites-with-playwright-in-python-developer-guide-49mh" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;13&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/alinashah/scrape-javascript-heavy-sites-with-playwright-in-python-developer-guide-49mh#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              10&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            9 min read
          &lt;/small&gt;
          
          &lt;div class="only-sidebar-menu-item trusted-visible-block print-hidden align-center"&gt;
                
                    
                
          &lt;/div&gt;

        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>automation</category>
      <category>python</category>
      <category>tutorial</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Scrape JavaScript-Heavy Sites with Playwright in Python (Developer Guide)</title>
      <dc:creator>Alina Shah</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:15:42 +0000</pubDate>
      <link>https://dev.to/alinashah/scrape-javascript-heavy-sites-with-playwright-in-python-developer-guide-49mh</link>
      <guid>https://dev.to/alinashah/scrape-javascript-heavy-sites-with-playwright-in-python-developer-guide-49mh</guid>
      <description>&lt;p&gt;You fire off a &lt;code&gt;requests.get()&lt;/code&gt;, parse the response with BeautifulSoup, and find an empty &lt;code&gt;&amp;lt;div id="root"&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt;. The data you can see in your browser never made it into the HTML. This is the everyday reality of scraping modern sites built with React, Vue, Angular, or any framework that renders content in the browser.&lt;/p&gt;

&lt;p&gt;Playwright solves this by driving a real browser engine. It runs the page's JavaScript, waits for the content to appear, and lets you extract it just as a user would see it. This guide walks through the full workflow in Python: setup, waiting strategies, infinite scroll, intercepting API calls, performance tuning, and making your scraper reliable enough to run unattended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why requests and BeautifulSoup fail on dynamic pages
&lt;/h2&gt;

&lt;p&gt;A traditional HTTP client downloads the initial HTML document and stops. On a client-side rendered site, that document is mostly a shell. The real content arrives later through JavaScript that fetches JSON from an API, builds DOM nodes, and injects them into the page.&lt;/p&gt;

&lt;p&gt;You can confirm this quickly. Open DevTools, choose "View Source" (not "Inspect"), and search for a piece of text you can see on screen. If it's missing from the source, the page is rendered by JavaScript, and you need either a browser or a way to call the underlying API directly. Playwright gives you both options.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up Playwright for Python
&lt;/h2&gt;

&lt;p&gt;Install the library and download the browser binaries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;playwright
playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Playwright also supports Firefox and WebKit, but Chromium is the usual choice for &lt;a href="https://www.maatechanalytics.com/" rel="noopener noreferrer"&gt;web scraping&lt;/a&gt; because it's the most widely tested and matches what most visitors use. Here is a minimal script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headless&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/products&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait_until&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domcontentloaded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.product-card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flow is always the same: launch a browser, open a page, navigate, wait for the content you need, extract, and close. The sync API is easiest for scripts. We'll switch to async later when concurrency matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wait for elements the right way
&lt;/h2&gt;

&lt;p&gt;Most flaky scrapers fail because of bad waiting. Three common mistakes: reading the DOM too early, sleeping for a fixed time, and relying on &lt;code&gt;networkidle&lt;/code&gt; for everything.&lt;/p&gt;

&lt;p&gt;Playwright's locators auto-wait. When you call &lt;code&gt;locator.click()&lt;/code&gt; or &lt;code&gt;locator.inner_text()&lt;/code&gt;, Playwright waits until the element exists, is visible, and is stable. This is the behavior you want in most cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h1.product-title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;inner_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you need to wait explicitly, choose the narrowest condition that proves the data is ready:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;page.wait_for_selector(".product-card")&lt;/code&gt; waits for the first item to render.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;page.wait_for_function("document.querySelectorAll('.product-card').length &amp;gt;= 20")&lt;/code&gt; waits for a specific amount of content.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;page.wait_for_load_state("networkidle")&lt;/code&gt; waits until there's no network activity for 500 ms. It's a blunt tool. Sites with analytics pings, WebSockets, or polling never go idle, so use it sparingly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid &lt;code&gt;time.sleep()&lt;/code&gt; and &lt;code&gt;page.wait_for_timeout()&lt;/code&gt; as your main strategy. A fixed delay is either too short and breaks, or too long and wastes time on every page. Use it only for deliberate pauses, such as spacing out requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extract data from the rendered DOM
&lt;/h2&gt;

&lt;p&gt;Once the content has rendered, you can pull it out with locators:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.product-card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
    &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;inner_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;inner_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works, but every &lt;code&gt;inner_text()&lt;/code&gt; call is a round trip between &lt;a href="https://dev.to/devstackcommunity/getting-started-with-python-web-scraping-tools-tips-code-d24"&gt;Python&lt;/a&gt; and the browser. On a page with 500 items and five fields each, that adds up. A faster approach is to run one JavaScript function on the page and return everything at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.product-card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;evaluate_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
  els =&amp;gt; els.map(el =&amp;gt; ({
    name: el.querySelector(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;h2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)?.textContent.trim(),
    price: el.querySelector(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)?.textContent.trim(),
    url: el.querySelector(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)?.href
  }))
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One call returns a list of dictionaries. For large pages, this is often ten times faster than looping over locators in Python. Use optional chaining (&lt;code&gt;?.&lt;/code&gt;) so a missing field returns &lt;code&gt;null&lt;/code&gt; instead of throwing an error and killing the whole extraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check for a hidden API before parsing HTML
&lt;/h2&gt;

&lt;p&gt;Before you write a single selector, open the Network tab in DevTools, filter by Fetch/XHR, and reload the page. Dynamic sites usually load their data from a JSON endpoint. If you can see that endpoint, you can often skip the HTML parsing entirely.&lt;/p&gt;

&lt;p&gt;Playwright lets you capture those responses while the page runs, which means you get clean JSON without reverse-engineering authentication or request signing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/products&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp_info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/products&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp_info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSON is far more stable than CSS selectors. Designers change class names constantly, but API response shapes tend to survive redesigns. When a clean API response is available, prefer it. Fall back to DOM extraction when the data is only assembled on the client or the API is heavily protected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle infinite scroll
&lt;/h2&gt;

&lt;p&gt;Infinite scroll loads more items as the user nears the bottom of the page. To scrape it, you scroll, wait for new content, and stop when nothing new appears. Counting items is more reliable than comparing page height:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nb"&gt;TimeoutError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;PWTimeout&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scroll_to_end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.product-card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_rounds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_rounds&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wheel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;([sel, prev]) =&amp;gt; document.querySelectorAll(sel).length &amp;gt; prev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;PWTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;  &lt;span class="c1"&gt;# no new items appeared, we've reached the end
&lt;/span&gt;        &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;previous&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loop scrolls, then waits up to four seconds for the item count to increase. If the timeout fires, the list has ended. The &lt;code&gt;max_rounds&lt;/code&gt; cap protects you from feeds that never stop.&lt;/p&gt;

&lt;p&gt;For "Load more" buttons, the pattern is similar. Click the button inside &lt;code&gt;expect_response&lt;/code&gt; or wait for the count to grow, and break when the button disappears or becomes disabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle pagination and multiple pages
&lt;/h2&gt;

&lt;p&gt;For classic pagination, loop until there is no next link. Always wait for the new page's content, not just for the click to finish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.product-card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.product-card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;evaluate_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXTRACT_JS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;next_btn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a[rel=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;next&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;next_btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;next_btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_load_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domcontentloaded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the URL contains a page number, it is usually faster and more robust to generate URLs directly and open them in parallel contexts than to click through one at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make it faster: block junk and go async
&lt;/h2&gt;

&lt;p&gt;A browser downloads images, fonts, video, and trackers you don't need. Blocking them cuts load time and bandwidth sharply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;block_heavy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource_type&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;font&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;media&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
        &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;continue_&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block_heavy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, reuse the browser and create lightweight contexts rather than launching a new browser per URL. A context is an isolated session with its own cookies and storage, and it starts in milliseconds. Combine this with the async API and a semaphore to control concurrency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.async_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;async_playwright&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scrape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait_until&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domcontentloaded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.product-card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.product-card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;evaluate_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXTRACT_JS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Semaphore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;async_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;scrape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start with five concurrent pages and increase slowly. Each page consumes real memory, often 100 to 300 MB, so concurrency is limited by RAM before it's limited by your network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build reliability into the scraper
&lt;/h2&gt;

&lt;p&gt;A script that works once is a prototype. A script that works every night is a pipeline. A few habits make the difference.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retry with backoff:&lt;/strong&gt; Network errors and timeouts are normal. Wrap navigation in a retry helper (the &lt;code&gt;tenacity&lt;/code&gt; library works well) and use exponential backoff so you don't hammer a struggling server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set explicit timeouts:&lt;/strong&gt; The default 30 seconds per action is generous. Set tighter limits with &lt;code&gt;page.set_default_timeout(10000)&lt;/code&gt; so a stuck page fails fast and moves to the retry queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate what you extract:&lt;/strong&gt; Check that required fields are present and that prices parse as numbers. A selector that silently returns &lt;code&gt;None&lt;/code&gt; on every row is the most common sign a site has changed. Fail loudly when the percentage of empty fields crosses a threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save as you go:&lt;/strong&gt; Write each batch to JSON Lines or a database as it arrives instead of holding everything in memory. If the run crashes at item 9,000, you keep the first 8,999.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be a polite client:&lt;/strong&gt; Respect &lt;code&gt;robots.txt&lt;/code&gt; where it applies, space out requests, and identify yourself sensibly. Aggressive scraping gets blocked faster, and it burdens the sites you depend on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Debug like a developer
&lt;/h2&gt;

&lt;p&gt;Playwright ships with excellent tooling. Three features are worth learning early:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Headed mode:&lt;/strong&gt; Launch with &lt;code&gt;headless=False&lt;/code&gt; and &lt;code&gt;slow_mo=250&lt;/code&gt; to watch the browser work step by step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codegen:&lt;/strong&gt; Run &lt;code&gt;playwright codegen https://example.com&lt;/code&gt; to record your clicks and generate locator code you can adapt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trace viewer:&lt;/strong&gt; Start a trace with &lt;code&gt;context.tracing.start(screenshots=True, snapshots=True)&lt;/code&gt;, save it on failure, and open it with &lt;code&gt;playwright show-trace trace.zip&lt;/code&gt;. You get a timeline of every action, a DOM snapshot at each step, and the network log. It is the fastest way to understand why a run failed on a server you can't watch.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Know the limits of browser scraping
&lt;/h2&gt;

&lt;p&gt;Playwright handles rendering well, but it isn't magic. Many sites run bot detection that examines TLS fingerprints, browser properties, request patterns, and IP reputation. A default headless browser from a datacenter IP will be flagged on protected sites regardless of how well your selectors work. Solving that usually involves residential or rotating proxies, realistic browser profiles, and careful pacing. It is a separate topic with its own trade-offs.&lt;/p&gt;

&lt;p&gt;Browsers are also expensive. If your target exposes a stable JSON API, calling it directly with &lt;code&gt;httpx&lt;/code&gt; will be orders of magnitude cheaper than rendering pages. Use Playwright to discover the API, then decide whether you still need the browser in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;Scraping JavaScript-heavy sites comes down to a handful of principles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check the Network tab first. If there's a clean API, use it.&lt;/li&gt;
&lt;li&gt;Rely on auto-waiting locators and wait for specific conditions rather than fixed delays.&lt;/li&gt;
&lt;li&gt;Extract in bulk with &lt;code&gt;evaluate_all&lt;/code&gt; instead of many small calls.&lt;/li&gt;
&lt;li&gt;Handle infinite scroll by counting items and stopping when growth ends.&lt;/li&gt;
&lt;li&gt;Block unneeded resources, reuse the browser, and cap concurrency.&lt;/li&gt;
&lt;li&gt;Validate output, retry failures, and save progress continuously.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With these patterns, Playwright turns the hardest category of pages into a manageable engineering problem. Start with one page, get the extraction right, then layer in scrolling, concurrency, and monitoring as the job grows. When maintaining proxies, browsers, and breakage handling starts to cost more than the data is worth, that's the point to consider a managed scraping service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I use AI to scrape websites with Playwright?
&lt;/h3&gt;

&lt;p&gt;Yes. Playwright handles the hard part for &lt;a href="https://www.maatechanalytics.com/services/ai-powered-web-scraping" rel="noopener noreferrer"&gt;AI-powered web scraping&lt;/a&gt;: it renders the JavaScript and gives you the final DOM. You then pass the cleaned content to an LLM and ask it to return structured JSON. This works well on messy or inconsistent pages where writing CSS selectors for every layout is impractical.&lt;/p&gt;

&lt;p&gt;A reliable pattern is to render the page, trim it to the relevant section, send it with a strict schema, and validate the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;inner_html&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# trim before sending
# send `html` (or its text) to your LLM with the schema,
# then validate the reply instead of trusting it:
&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_validate_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm_response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trade-offs are cost, latency, and occasional wrong values. For high-volume pages with a stable layout, CSS selectors or a hidden JSON API are cheaper and more predictable. Many teams use selectors as the default and call the LLM only as a fallback when extraction fails or the layout varies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use Playwright, Selenium, or Puppeteer for scraping?
&lt;/h3&gt;

&lt;p&gt;For new Python projects, Playwright is usually the better choice. It has built-in auto-waiting, cleaner async support, isolated browser contexts, and tools like codegen and the trace viewer. Selenium is still reasonable if you have a large existing test suite or need its wider ecosystem. Puppeteer is a solid option if your stack is Node.js, since Playwright's Python bindings are the main reason to pick it for Python work.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I scrape pages that require a login?
&lt;/h3&gt;

&lt;p&gt;Log in once, save the session state, and reuse it so you don't authenticate on every run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# One time: log in, then save cookies and local storage
&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;storage_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auth.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Later runs: start already authenticated
&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storage_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auth.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only scrape content you're authorized to access, and check the site's terms first. Sessions expire, so add a check that detects a logged-out page and refreshes the state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does my scraper work locally but fail on a server?
&lt;/h3&gt;

&lt;p&gt;The usual causes are missing system dependencies, a datacenter IP that gets flagged, and low memory. Install the browser with its OS dependencies using &lt;code&gt;playwright install --with-deps chromium&lt;/code&gt;, or build from the official Playwright Python Docker image. If pages load but return blocks or CAPTCHAs, the problem is IP reputation and browser fingerprinting, not your code. Also check RAM, because each page can use 100 to 300 MB.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I avoid getting blocked while scraping?
&lt;/h3&gt;

&lt;p&gt;Start with the basics: lower your concurrency, add delays between requests, block assets you don't need, and reuse sessions where it makes sense. Prefer an official or hidden API over rendering pages when one exists. If a site still blocks you, you're likely dealing with fingerprinting or IP reputation, which usually needs rotating proxies and more realistic browser profiles. Always respect &lt;code&gt;robots.txt&lt;/code&gt; and the site's terms, and stop if a site clearly doesn't want automated access.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>python</category>
      <category>playwright</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Python web scraping is a must-have skill for developers working with data! 🐍 From choosing the right tools to writing your first scraper, this guide covers the essentials with practical tips and code examples. 🚀

Check it out 👇</title>
      <dc:creator>Alina Shah</dc:creator>
      <pubDate>Tue, 27 Oct 2026 17:28:04 +0000</pubDate>
      <link>https://dev.to/alinashah/python-web-scraping-is-a-must-have-skill-for-developers-working-with-data-from-choosing-the-4pf0</link>
      <guid>https://dev.to/alinashah/python-web-scraping-is-a-must-have-skill-for-developers-working-with-data-from-choosing-the-4pf0</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/devstackcommunity/getting-started-with-python-web-scraping-tools-tips-code-d24" class="crayons-story__hidden-navigation-link"&gt;Getting Started With Python Web Scraping: Tools, Tips &amp;amp; Code&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/devstackcommunity"&gt;
            &lt;img alt="Dev Stack Community logo" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14438%2F85a1a7c8-0220-43be-97c4-8dff708b6f1a.png" class="crayons-logo__image" width="800" height="800"&gt;
          &lt;/a&gt;

          &lt;a href="/alinashah" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4139861%2Fad406f4f-1a76-4259-9ba8-42437ccdbfc3.jpg" alt="alinashah profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/alinashah" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Alina Shah
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Alina Shah
                
                
              
              &lt;div id="story-author-preview-content-4929909" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/alinashah" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4139861%2Fad406f4f-1a76-4259-9ba8-42437ccdbfc3.jpg" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Alina Shah&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/devstackcommunity" class="crayons-story__secondary fw-medium"&gt;Dev Stack Community&lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/devstackcommunity/getting-started-with-python-web-scraping-tools-tips-code-d24" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 27&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/devstackcommunity/getting-started-with-python-web-scraping-tools-tips-code-d24" id="article-link-4929909"&gt;
          Getting Started With Python Web Scraping: Tools, Tips &amp;amp; Code
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/discuss"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;discuss&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/python"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;python&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webscraping"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webscraping&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/devstackcommunity/getting-started-with-python-web-scraping-tools-tips-code-d24" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/devstackcommunity/getting-started-with-python-web-scraping-tools-tips-code-d24#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            10 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>beginners</category>
      <category>python</category>
      <category>tutorial</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Getting Started With Python Web Scraping: Tools, Tips &amp; Code</title>
      <dc:creator>Alina Shah</dc:creator>
      <pubDate>Tue, 27 Oct 2026 17:21:13 +0000</pubDate>
      <link>https://dev.to/devstackcommunity/getting-started-with-python-web-scraping-tools-tips-code-d24</link>
      <guid>https://dev.to/devstackcommunity/getting-started-with-python-web-scraping-tools-tips-code-d24</guid>
      <description>&lt;p&gt;Have you ever wanted to collect product prices from multiple websites, track job listings, or gather article titles without copying everything manually?&lt;/p&gt;

&lt;p&gt;That is where Python web scraping comes in. Instead of opening dozens of pages and transferring information into a spreadsheet, you can write a script to retrieve web pages, extract the information you need, and organize it into a usable format.&lt;/p&gt;

&lt;p&gt;The tricky part is that websites don't all store information the same way. Some use straightforward HTML, while others load content with JavaScript, block automated requests, or change their page structure without warning.&lt;/p&gt;

&lt;p&gt;In this guide, I will walk through essential Python &lt;a href="https://dev.to/devstackhub/top-13-web-scraping-tools-every-developer-should-know-31p4"&gt;web scraping tools&lt;/a&gt;, show working code examples, and explain how to build easier-to-maintain scrapers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Python Web Scraping?
&lt;/h2&gt;

&lt;p&gt;Python web scraping is the process of using Python code to retrieve information from web pages and extract specific data from their content.&lt;/p&gt;

&lt;p&gt;For example, suppose you want to collect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product names and prices from a permitted product listing page.&lt;/li&gt;
&lt;li&gt;Job titles and company names from public job listings.&lt;/li&gt;
&lt;li&gt;Article headlines and publication dates from a news archive.&lt;/li&gt;
&lt;li&gt;Publicly available research data from a website.&lt;/li&gt;
&lt;li&gt;Links and metadata from a directory of resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You could copy the information manually, but that becomes inefficient when you need hundreds of records or want to repeat the process regularly.&lt;/p&gt;

&lt;p&gt;A scraper automates the workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Send a request to a web page.&lt;/li&gt;
&lt;li&gt;Receive the page content.&lt;/li&gt;
&lt;li&gt;Parse the HTML.&lt;/li&gt;
&lt;li&gt;Identify the elements containing the information.&lt;/li&gt;
&lt;li&gt;Extract and clean the values.&lt;/li&gt;
&lt;li&gt;Save the results to a file or database.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Python is a popular choice because its libraries make HTTP requests, HTML parsing, browser automation, and data processing relatively straightforward.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Best Python Web Scraping Tools to Know
&lt;/h2&gt;

&lt;p&gt;You do not need a complicated stack to start scraping. The tools you choose should depend on how the target website delivers its content.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Requests: Retrieve Web Pages
&lt;/h3&gt;

&lt;p&gt;The Requests library sends HTTP requests and provides a convenient way to access responses.&lt;/p&gt;

&lt;p&gt;It is a good starting point when the information you need is already present in the page's HTML.&lt;/p&gt;

&lt;p&gt;Install it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A basic example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;get()&lt;/code&gt; method requests the page. The &lt;code&gt;timeout&lt;/code&gt; prevents the request from waiting indefinitely, while &lt;code&gt;raise_for_status()&lt;/code&gt; raises an exception for unsuccessful HTTP responses.&lt;/p&gt;

&lt;p&gt;For real projects, handle network errors and timeouts instead of assuming every request will succeed.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Beautiful Soup: Extract Information From HTML
&lt;/h3&gt;

&lt;p&gt;A successful HTTP request does not automatically give you clean data. The response usually contains HTML with headings, links, paragraphs, navigation, and other elements.&lt;/p&gt;

&lt;p&gt;Beautiful Soup helps you parse that HTML and select the elements you need.&lt;/p&gt;

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;beautifulsoup4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's extract the title and headings from a page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;page_title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No title found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Page title:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;heading&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example demonstrates two useful techniques: checking whether an element exists before accessing it and using &lt;code&gt;get_text()&lt;/code&gt; to remove the HTML markup from the extracted content.&lt;/p&gt;

&lt;p&gt;Beautiful Soup is especially useful for small and medium scraping tasks involving predictable HTML.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. lxml: Parse HTML Efficiently
&lt;/h3&gt;

&lt;p&gt;The lxml library is another option for parsing HTML and XML. It supports XPath expressions, which can be useful when you need to select elements using more detailed structural rules.&lt;/p&gt;

&lt;p&gt;Install it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;lxml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use it directly with BeautifulSoup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;

&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
&amp;lt;html&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;Python Web Scraping&amp;lt;/h1&amp;gt;
    &amp;lt;p class=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;A beginner&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s guide&amp;lt;/p&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lxml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;heading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For many beginners, Beautiful Soup with Python's built-in HTML parser is enough. Consider lxml when you need XPath or want to evaluate parsing performance for a larger workload.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Scrapy: Build Larger Scraping Projects
&lt;/h3&gt;

&lt;p&gt;Scrapy is a more complete framework for crawling websites and extracting data across multiple pages. It provides request scheduling, item processing, pipelines, and result exporting.&lt;/p&gt;

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;scrapy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scrapy startproject my_scraper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scrapy becomes useful when a project needs to follow pagination, visit many links, manage retries, and process a substantial number of records.&lt;/p&gt;

&lt;p&gt;For a single page, it may be unnecessary. For a recurring crawl with several stages, its built-in structure can save time compared with writing everything from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Playwright: Handle JavaScript-Rendered Pages
&lt;/h3&gt;

&lt;p&gt;Some websites display their main content only after JavaScript runs. A simple Requests call may return an HTML shell without the data you expected.&lt;/p&gt;

&lt;p&gt;Playwright automates a browser, allowing you to wait for page content and interact with elements when permitted.&lt;/p&gt;

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;playwright
playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headless&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;wait_until&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domcontentloaded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;page_title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example opens a page in a browser and retrieves its title. If you need content loaded later by JavaScript, wait for a specific, documented page element rather than adding an arbitrary long delay.&lt;/p&gt;

&lt;p&gt;Browser automation consumes more resources than a basic HTTP request, so use it only when the target page actually requires it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Complete Python Web Scraping Example
&lt;/h2&gt;

&lt;p&gt;Let's combine &lt;a href="https://dev.toRequests%20and%20Beautiful%20Soup"&gt;Requests and Beautiful Soup&lt;/a&gt; into a small scraper.&lt;/p&gt;

&lt;p&gt;For this example, we will use &lt;a href="https://books.toscrape.com/" rel="noopener noreferrer"&gt;Books to Scrape&lt;/a&gt;, a demonstration website intended for scraping practice. The script retrieves book titles and prices from its first catalog page.&lt;/p&gt;

&lt;p&gt;Install the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;requests beautifulsoup4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create a Python file named &lt;code&gt;scrape_books.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;

&lt;span class="n"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://books.toscrape.com/catalogue/page-1.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LearningScraper/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scrape_books&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;article.product_pod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;title_element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h3 a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;price_element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.price_color&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;title_element&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;price_element&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title_element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;price_element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_to_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;books.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;fieldnames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeheader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;scrape_books&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;save_to_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Saved &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; books to books.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python scrape_books.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the request succeeds and the page structure matches the selectors, the script creates a &lt;code&gt;books.csv&lt;/code&gt; file containing the extracted titles and prices.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the Code Works
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Request the page.&lt;/strong&gt; Requests downloads the HTML from the catalog URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Parse the HTML.&lt;/strong&gt; Beautiful Soup converts the response into a structure that Python can search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Find book listings.&lt;/strong&gt; The CSS selector &lt;code&gt;article.product_pod&lt;/code&gt; targets each book card.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Extract fields.&lt;/strong&gt; The script retrieves the book title from the link's &lt;code&gt;title&lt;/code&gt; attribute and the displayed price from the price element.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Save the results.&lt;/strong&gt; Python's built-in &lt;code&gt;csv&lt;/code&gt; module writes the records to a spreadsheet-friendly file.&lt;/p&gt;

&lt;p&gt;The script also checks whether the expected elements exist. This matters because a missing element should not crash the entire scraping process unnecessarily.&lt;/p&gt;

&lt;p&gt;The example is deliberately limited to one page. A production scraper would need additional handling for pagination, unexpected page layouts, network failures, and website changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Choose the Right Python Scraping Tool
&lt;/h2&gt;

&lt;p&gt;Use this quick guide when deciding which library to start with.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Best use case&lt;/th&gt;
&lt;th&gt;Learning curve&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requests&lt;/td&gt;
&lt;td&gt;Downloading HTML&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Beautiful Soup&lt;/td&gt;
&lt;td&gt;Extracting text, links, and attributes&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lxml&lt;/td&gt;
&lt;td&gt;HTML/XML parsing and XPath&lt;/td&gt;
&lt;td&gt;Low to moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scrapy&lt;/td&gt;
&lt;td&gt;Multi-page crawling and structured scraping pipelines&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;Pages that require browser rendering or interaction&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a first project, I would start with Requests and Beautiful Soup. They cover most straightforward scraping tasks without adding unnecessary complexity.&lt;/p&gt;

&lt;p&gt;Move to Scrapy when the project grows into a crawler. Use Playwright when you confirm that the information depends on browser execution or interaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Tips for More Reliable Web Scraping
&lt;/h2&gt;

&lt;p&gt;A script that works once is not necessarily a reliable scraper. Small changes to the website, network conditions, or response format can break it.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Check the Website's Rules First
&lt;/h3&gt;

&lt;p&gt;Before collecting data, review the website's terms, applicable policies, and &lt;code&gt;robots.txt&lt;/code&gt; guidance. Prefer an official API or permitted data export when available.&lt;/p&gt;

&lt;p&gt;Do not assume that publicly visible information is automatically free to collect or reuse. Respect access restrictions, copyright, privacy requirements, and rate limits. Do not bypass authentication, CAPTCHAs, or other technical controls without authorization.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Add Timeouts and Error Handling
&lt;/h3&gt;

&lt;p&gt;Network requests can fail because of timeouts, DNS problems, server errors, or temporary connectivity issues.&lt;/p&gt;

&lt;p&gt;A simple pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The request timed out.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Request failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For recurring jobs, record errors in logs and decide which failures are safe to retry. Respect server-provided rate-limit information and avoid retrying indefinitely.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Use Specific CSS Selectors
&lt;/h3&gt;

&lt;p&gt;Broad selectors can collect unrelated information. For example, selecting every paragraph may capture navigation labels, advertisements, and footer text.&lt;/p&gt;

&lt;p&gt;Inspect the page's HTML and target the elements that correspond to the data you actually need.&lt;/p&gt;

&lt;p&gt;When possible, use stable attributes or semantic elements. Avoid depending on automatically generated CSS class names that may change frequently.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Handle Missing and Inconsistent Data
&lt;/h3&gt;

&lt;p&gt;Not every record will contain every field. Prices might be missing, publication dates might use different formats, or a page might have an unexpected layout.&lt;/p&gt;

&lt;p&gt;Check for missing elements before extracting values. Normalize whitespace, dates, and numbers, and validate the resulting data before saving it.&lt;/p&gt;

&lt;p&gt;If the output will feed a dashboard or database, add tests for the fields that must always be present.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Avoid Sending Too Many Requests
&lt;/h3&gt;

&lt;p&gt;High request volumes can overload a website and cause your IP address to be blocked. Keep request rates reasonable, follow published limits, and cache responses when appropriate.&lt;/p&gt;

&lt;p&gt;For a small sequential scraper, even a short delay between requests may help reduce unnecessary load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;permitted_urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Fetch and process the page here.
&lt;/span&gt;    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a basic pacing example, not a guarantee that one request per second is permitted. Follow the target site's actual requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Keep Scraped Data Separate From Your Code
&lt;/h3&gt;

&lt;p&gt;Save collected records to CSV, JSON, or a database instead of mixing data extraction with every downstream task.&lt;/p&gt;

&lt;p&gt;This makes it easier to inspect the results, rerun analysis without downloading the pages again, and identify whether a problem occurred during retrieval, parsing, or storage.&lt;/p&gt;

&lt;p&gt;For recurring projects, record when the data was collected and where it came from. This provides useful context when values change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Python Web Scraping Problems and Fixes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The script returns an empty list:&lt;/strong&gt; Check the page source and confirm that your CSS selectors match the actual HTML. The data might be loaded through JavaScript, or the page may have returned an error message instead of the expected content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The request returns a 403 or 429 response:&lt;/strong&gt; The server may be denying access or enforcing rate limits. Review the site's rules and reduce request frequency where appropriate. Do not attempt to bypass access controls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The extracted text contains extra whitespace:&lt;/strong&gt; Use methods such as &lt;code&gt;get_text(" ", strip=True)&lt;/code&gt; and normalize the values before saving them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The scraper stops when one element is missing:&lt;/strong&gt; Check for &lt;code&gt;None&lt;/code&gt; before accessing an element's attributes or methods, and log records that do not match the expected structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The scraper becomes slow:&lt;/strong&gt; Avoid launching a browser for pages that work with HTTP requests. For larger crawls, consider Scrapy's request scheduling and concurrency controls, configured to respect the target website's limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The website changes its layout:&lt;/strong&gt; Add tests that verify important selectors and expected fields. When a scraper runs regularly, monitor record counts and unexpected missing values so structural changes do not go unnoticed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When Should You Use an API Instead of Web Scraping?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.maatechanalytics.com/" rel="noopener noreferrer"&gt;Web scraping&lt;/a&gt; is not always the best solution. If a website provides an official API, that is usually the first option to investigate.&lt;/p&gt;

&lt;p&gt;An API typically provides structured responses, documented fields, and clearer expectations about access and rate limits. Scraping may be useful when the information is available only in HTML, and you are permitted to collect it.&lt;/p&gt;

&lt;p&gt;Before building a scraper, ask three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is there an official API or downloadable dataset?&lt;/li&gt;
&lt;li&gt;Am I permitted to access, collect, and use this information?&lt;/li&gt;
&lt;li&gt;Can I maintain the scraper if the page structure changes?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If an API provides the data you need, it may be more reliable and easier to maintain than parsing HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Python web scraping is a practical way to automate data collection, whether you are learning to extract a few headlines or building a recurring data pipeline. Requests and Beautiful Soup are excellent starting points for ordinary HTML pages, while lxml, Scrapy, and Playwright address more specialized needs.&lt;/p&gt;

&lt;p&gt;The biggest lesson is to keep the first project simple. Choose one permitted page, inspect its HTML, extract a small set of fields, and save the results to CSV. Once that works, add error handling, validation, logging, and sensible request limits.&lt;/p&gt;

&lt;p&gt;Reliable scraping is not just about getting data out of a page. It is about collecting the right information responsibly and building a script that keeps working when conditions change.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Python good for web scraping?
&lt;/h3&gt;

&lt;p&gt;Yes. Python has libraries for HTTP requests, HTML parsing, browser automation, crawling, and data processing. Beginners can start with Requests and Beautiful Soup, then use tools such as Scrapy or Playwright when their requirements become more complex.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which Python library is best for web scraping?
&lt;/h3&gt;

&lt;p&gt;Beautiful Soup is a good choice for extracting information from HTML, usually alongside Requests. Scrapy suits larger crawling projects, while Playwright is useful when content requires browser rendering or interaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Python scrape JavaScript websites?
&lt;/h3&gt;

&lt;p&gt;Yes, but the right method depends on how the website delivers its content. If the data is available through a permitted underlying API or in the initial HTML, a normal HTTP request may be enough. If the page requires browser-side JavaScript, a tool such as Playwright can render it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is web scraping legal?
&lt;/h3&gt;

&lt;p&gt;It depends on the website, the data, the access method, and the intended use. Review the site's terms, access rules, privacy obligations, and applicable laws. Public visibility alone does not establish permission to collect or reuse all content.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I save scraped data in Python?
&lt;/h3&gt;

&lt;p&gt;You can save scraped records to CSV using Python's built-in &lt;code&gt;csv&lt;/code&gt; module or to JSON using the &lt;code&gt;json&lt;/code&gt; module. For larger or recurring projects, a database may be a better fit.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>programming</category>
      <category>discuss</category>
    </item>
    <item>
      <title>AI design-to-code tools are evolving fast, but not all deliver the same results. This comparison explores 11 tools, their key differences, and how they perform in real-world projects to help developers choose the right workflow.</title>
      <dc:creator>Alina Shah</dc:creator>
      <pubDate>Tue, 27 Oct 2026 17:02:45 +0000</pubDate>
      <link>https://dev.to/alinashah/ai-design-to-code-tools-are-evolving-fast-but-not-all-deliver-the-same-results-this-comparison-43d7</link>
      <guid>https://dev.to/alinashah/ai-design-to-code-tools-are-evolving-fast-but-not-all-deliver-the-same-results-this-comparison-43d7</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/devstackhub/i-compared-11-best-ai-design-to-code-tools-for-my-client-projects-4alo" class="crayons-story__hidden-navigation-link"&gt;I Compared 11 Best AI Design-to-Code Tools for My Client Projects&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/devstackhub" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4081963%2F9d10c5fa-c76e-4c9d-88af-e3f2fced5a62.jpg" alt="devstackhub profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/devstackhub" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Dev Stack Hub
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Dev Stack Hub
                
                
              
              &lt;div id="story-author-preview-content-4923481" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/devstackhub" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4081963%2F9d10c5fa-c76e-4c9d-88af-e3f2fced5a62.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Dev Stack Hub&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/devstackhub/i-compared-11-best-ai-design-to-code-tools-for-my-client-projects-4alo" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 26&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/devstackhub/i-compared-11-best-ai-design-to-code-tools-for-my-client-projects-4alo" id="article-link-4923481"&gt;
          I Compared 11 Best AI Design-to-Code Tools for My Client Projects
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/discuss"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;discuss&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/design"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;design&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/nocode"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;nocode&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/devstackhub/i-compared-11-best-ai-design-to-code-tools-for-my-client-projects-4alo" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;59&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/devstackhub/i-compared-11-best-ai-design-to-code-tools-for-my-client-projects-4alo#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              45&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            13 min read
          &lt;/small&gt;
          
          &lt;div class="only-sidebar-menu-item trusted-visible-block print-hidden align-center"&gt;
                
                    
                
          &lt;/div&gt;

        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>ai</category>
      <category>frontend</category>
      <category>tools</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What's your favorite AI site builder right now?</title>
      <dc:creator>Alina Shah</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:49:02 +0000</pubDate>
      <link>https://dev.to/alinashah/whats-your-favorite-ai-site-builder-right-now-3fm9</link>
      <guid>https://dev.to/alinashah/whats-your-favorite-ai-site-builder-right-now-3fm9</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/devstackhub/top-13-web-scraping-tools-every-developer-should-know-31p4" class="crayons-story__hidden-navigation-link"&gt;Top 13 Web Scraping Tools Every Developer Should Know&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/devstackhub" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4081963%2F9d10c5fa-c76e-4c9d-88af-e3f2fced5a62.jpg" alt="devstackhub profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/devstackhub" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Dev Stack Hub
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Dev Stack Hub
                
                
              
              &lt;div id="story-author-preview-content-4883601" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/devstackhub" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4081963%2F9d10c5fa-c76e-4c9d-88af-e3f2fced5a62.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Dev Stack Hub&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/devstackhub/top-13-web-scraping-tools-every-developer-should-know-31p4" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 20&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/devstackhub/top-13-web-scraping-tools-every-developer-should-know-31p4" id="article-link-4883601"&gt;
          Top 13 Web Scraping Tools Every Developer Should Know
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/discuss"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;discuss&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webscraping"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webscraping&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/productivity"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;productivity&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/devstackhub/top-13-web-scraping-tools-every-developer-should-know-31p4" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;32&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/devstackhub/top-13-web-scraping-tools-every-developer-should-know-31p4#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              19&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            13 min read
          &lt;/small&gt;
          
          &lt;div class="only-sidebar-menu-item trusted-visible-block print-hidden align-center"&gt;
                
                    
                
          &lt;/div&gt;

        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>The article is a practical, code-based guide to making web scrapers reliable in production. It covers timeouts and bounded retries, data validation, restart-safe job state, idempotent storage, and monitoring, with Python examples and an FAQ. It’s original,</title>
      <dc:creator>Alina Shah</dc:creator>
      <pubDate>Thu, 15 Oct 2026 17:57:21 +0000</pubDate>
      <link>https://dev.to/alinashah/the-article-is-a-practical-code-based-guide-to-making-web-scrapers-reliable-in-production-it-8f4</link>
      <guid>https://dev.to/alinashah/the-article-is-a-practical-code-based-guide-to-making-web-scrapers-reliable-in-production-it-8f4</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/alinashah/how-i-built-a-web-scraper-that-doesnt-break-in-production-4ki0" class="crayons-story__hidden-navigation-link"&gt;How I Built a Web Scraper That Doesn’t Break in Production&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/alinashah" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4139861%2Fad406f4f-1a76-4259-9ba8-42437ccdbfc3.jpg" alt="alinashah profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/alinashah" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Alina Shah
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Alina Shah
                
                
              
              &lt;div id="story-author-preview-content-4852300" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/alinashah" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4139861%2Fad406f4f-1a76-4259-9ba8-42437ccdbfc3.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Alina Shah&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/alinashah/how-i-built-a-web-scraper-that-doesnt-break-in-production-4ki0" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 14&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/alinashah/how-i-built-a-web-scraper-that-doesnt-break-in-production-4ki0" id="article-link-4852300"&gt;
          How I Built a Web Scraper That Doesn’t Break in Production
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/discuss"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;discuss&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webscraping"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webscraping&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/productivity"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;productivity&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devdiscuss"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devdiscuss&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/alinashah/how-i-built-a-web-scraper-that-doesnt-break-in-production-4ki0" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;15&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/alinashah/how-i-built-a-web-scraper-that-doesnt-break-in-production-4ki0#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              8&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            11 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>automation</category>
      <category>python</category>
      <category>tutorial</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>How I Built a Web Scraper That Doesn’t Break in Production</title>
      <dc:creator>Alina Shah</dc:creator>
      <pubDate>Wed, 14 Oct 2026 18:57:00 +0000</pubDate>
      <link>https://dev.to/alinashah/how-i-built-a-web-scraper-that-doesnt-break-in-production-4ki0</link>
      <guid>https://dev.to/alinashah/how-i-built-a-web-scraper-that-doesnt-break-in-production-4ki0</guid>
      <description>&lt;p&gt;The first version of my web scraper worked perfectly. It sent requests, extracted the data I needed, and saved everything to a database. I tested it on a few pages, checked the output, and assumed the difficult part was over. &lt;/p&gt;

&lt;p&gt;Then I ran it against a larger dataset. Some pages returned errors, a few records were missing, the website changed its HTML structure, and the scraper quietly saved incomplete data. &lt;/p&gt;

&lt;p&gt;The problem wasn't that I couldn't write a scraper. It was that I had built a script that worked under ideal conditions, not a system that could survive real-world conditions. &lt;/p&gt;

&lt;p&gt;That changed how I approached &lt;a href="https://www.maatechanalytics.com/services/web-scraping" rel="noopener noreferrer"&gt;web scraping&lt;/a&gt;. Instead of focusing only on extracting data quickly, I started designing for failures, retries, monitoring, and recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Most Web Scrapers Break in Production
&lt;/h2&gt;

&lt;p&gt;A scraper can look reliable during development because the test environment is usually predictable. You know which URL to request, the website is responding, and the page structure hasn't changed. Production is different.&lt;/p&gt;

&lt;p&gt;Websites change their layouts, networks fail, requests get throttled, and individual pages return unexpected content. Even a successful HTTP response doesn't guarantee that the page contains the data your scraper expects.&lt;/p&gt;

&lt;p&gt;These were the problems I needed to solve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unstable requests:&lt;/strong&gt; Temporary network failures, timeouts, and server errors interrupted scraping jobs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting:&lt;/strong&gt; Sending too many requests in a short period triggered HTTP 429 responses or temporary blocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changing HTML:&lt;/strong&gt; A class name or page structure changed, and extraction started returning empty values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incomplete records:&lt;/strong&gt; Some pages loaded partially, or optional fields were missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate data:&lt;/strong&gt; Restarting a failed job sometimes caused the same records to be saved more than once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent failures:&lt;/strong&gt; The script kept running, but the amount or quality of extracted data dropped without an obvious error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I realized that fixing individual exceptions wasn't enough. I needed a scraper with clear failure-handling rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. I Started With a Simple, Maintainable Architecture
&lt;/h2&gt;

&lt;p&gt;My first improvement was separating the scraper into smaller components. Earlier, one function handled requests, parsing, validation, and database writes. That made debugging difficult because every failure could originate from several places.&lt;/p&gt;

&lt;p&gt;I split the workflow into five stages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;URL discovery:&lt;/strong&gt; Identify the pages that need to be processed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request handling:&lt;/strong&gt; Fetch pages with timeouts, headers, and controlled retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parsing:&lt;/strong&gt; Extract the required fields from the response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation:&lt;/strong&gt; Check whether the extracted data is complete and usable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; Save validated records without creating duplicates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation made failures easier to diagnose. If a page loaded successfully but returned no product title, I could investigate the parser rather than rewriting the request logic.&lt;/p&gt;

&lt;p&gt;It also made testing easier. I could test the parser with saved HTML files without making a network request every time.&lt;/p&gt;

&lt;p&gt;For a small project, I didn't need a complicated distributed system. A straightforward &lt;a href="https://www.python.org/about/apps/" rel="noopener noreferrer"&gt;Python application&lt;/a&gt; with a few focused modules was enough. The important decision was keeping responsibilities separate so that one component could change without breaking the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. I Added Timeouts and Retries, but With Limits
&lt;/h2&gt;

&lt;p&gt;One of the earliest lessons was that requests should never be allowed to wait indefinitely.&lt;/p&gt;

&lt;p&gt;A server might respond slowly, a connection could stall, or a network request might fail temporarily. Without a timeout, a worker could remain stuck and prevent the job from finishing.&lt;/p&gt;

&lt;p&gt;I used Python's requests library for straightforward HTTP requests and added explicit connection and read timeouts. I also introduced limited retries for temporary failures.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;requests.adapters&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPAdapter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib3.util.retry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Retry&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;retry_policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;backoff_factor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status_forcelist&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;allowed_methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;respect_retry_after_header&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HTTPAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;retry_policy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ResearchScraper/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timeout tuple sets a connection timeout of five seconds and a read timeout of twenty seconds. The retry policy handles selected temporary errors, with backoff to avoid immediately repeating requests.&lt;/p&gt;

&lt;p&gt;This was a useful starting point, but retries aren't a universal fix. A missing page, invalid URL, or persistent permission error usually won't become successful just because the scraper tries again.&lt;/p&gt;

&lt;p&gt;I also treated &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429" rel="noopener noreferrer"&gt;HTTP 429 responses&lt;/a&gt; carefully. When a server provides a Retry-After header, the scraper should respect it. Repeatedly hitting the same endpoint after being rate-limited only makes the problem worse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; retries should be bounded, selective, and observable. Infinite retries can turn a temporary failure into a job that never finishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. I Stopped Treating Successful Responses as Valid Data
&lt;/h2&gt;

&lt;p&gt;This was one of the most important changes.&lt;/p&gt;

&lt;p&gt;Initially, I assumed that if a request returned a successful status code, the page was ready to parse. But websites can return a normal HTML page containing a maintenance message, an access-denied screen, an empty result, or a completely redesigned layout.&lt;/p&gt;

&lt;p&gt;The request can succeed while the extraction fails.&lt;/p&gt;

&lt;p&gt;I added validation rules for the fields that mattered to the project. For example, if a product page was expected to contain a title and product URL, those fields had to be present before the record could be stored.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;title_element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h1.product-title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;price_element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.product-price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;title_element&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Required product title missing on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title_element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Product title is empty on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;price_element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;price_element&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The selectors here are illustrative. Real selectors depend on the target website and should be verified against its current HTML.&lt;/p&gt;

&lt;p&gt;Notice that I treated the title as required but the price as optional. That distinction matters. If every missing field causes the entire record to fail, a scraper can discard useful data unnecessarily. If no missing field triggers an error, broken extraction can go unnoticed.&lt;/p&gt;

&lt;p&gt;I classified fields into three groups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Required:&lt;/strong&gt; The record isn't useful without them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional:&lt;/strong&gt; The record can still be stored if they're missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Derived:&lt;/strong&gt; The scraper calculates them from other values, so they need their own validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also tracked validation failures separately from request failures. That made it much easier to identify whether a website was unavailable or my extraction logic had become outdated.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. I Designed for Website Changes
&lt;/h2&gt;

&lt;p&gt;HTML scraping is fragile by nature. A site owner can change a class name, move a field into a different element, or redesign the page without warning.&lt;/p&gt;

&lt;p&gt;I couldn't prevent those changes, so I made them easier to detect.&lt;/p&gt;

&lt;p&gt;First, I avoided relying on long, deeply nested CSS selectors when a simpler selector was available. I also kept selectors in a small, identifiable part of the code rather than scattering them across the application.&lt;/p&gt;

&lt;p&gt;Second, I saved representative HTML fixtures for automated parser tests. Whenever I changed the extraction logic, I ran the parser against those saved pages to confirm that titles, prices, links, and other fields were still extracted correctly.&lt;/p&gt;

&lt;p&gt;Third, I monitored extraction quality. A scraper that normally extracts 95% of expected titles shouldn't suddenly drop to 20% without triggering an alert.&lt;/p&gt;

&lt;p&gt;For example, I could track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Percentage of pages fetched successfully&lt;/li&gt;
&lt;li&gt;Percentage of pages producing valid records&lt;/li&gt;
&lt;li&gt;Number of missing required fields&lt;/li&gt;
&lt;li&gt;Number of records extracted per run&lt;/li&gt;
&lt;li&gt;Changes in the frequency of parsing errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A drop in extraction quality is often more useful as an early warning than a basic uptime check.&lt;/p&gt;

&lt;p&gt;I also kept a small collection of known-good test pages. When a website changed, I could compare the new HTML with previous samples and update the parser deliberately instead of guessing at the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. I Made the Scraper Safe to Restart
&lt;/h2&gt;

&lt;p&gt;A production job will eventually stop unexpectedly. The machine may restart, a database connection may fail, or a deployment may interrupt a running process.&lt;/p&gt;

&lt;p&gt;I wanted to restart the scraper without manually figuring out which URLs had already been processed.&lt;/p&gt;

&lt;p&gt;I added a record of each URL's processing state. A simplified workflow looked like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pending&lt;/td&gt;
&lt;td&gt;The URL is waiting to be processed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In progress&lt;/td&gt;
&lt;td&gt;A worker is handling the URL.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Completed&lt;/td&gt;
&lt;td&gt;Data was extracted, validated, and stored.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry scheduled&lt;/td&gt;
&lt;td&gt;A temporary failure may be recoverable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed&lt;/td&gt;
&lt;td&gt;The URL exceeded its retry limit or needs investigation.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The scraper could resume from pending and retry-scheduled URLs instead of starting from the beginning.&lt;/p&gt;

&lt;p&gt;I also made database writes idempotent. In practical terms, processing the same page twice shouldn't create two copies of the same record. A stable source ID or canonical URL, combined with a database uniqueness constraint and an upsert operation, can help enforce this.&lt;/p&gt;

&lt;p&gt;For example, a product record could use its source URL as a unique key if that URL is stable and reliably identifies the product. If the website provides a durable product ID, that may be a better choice.&lt;/p&gt;

&lt;p&gt;I didn't rely only on an in-memory list of completed URLs because that information disappears when the process stops. Persistent job state made recovery much more predictable.&lt;/p&gt;

&lt;p&gt;For larger scraping jobs, I would also add a lease or expiration time to the in-progress state. That allows the system to recover URLs that were assigned to a worker that crashed before finishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. I Controlled Concurrency Instead of Sending Requests as Fast as Possible
&lt;/h2&gt;

&lt;p&gt;It is tempting to increase the number of workers and send as many requests as possible. That can improve throughput, but excessive concurrency also increases failures, rate limiting, and pressure on the target server.&lt;/p&gt;

&lt;p&gt;I treated concurrency as a configurable limit rather than an unlimited performance setting.&lt;/p&gt;

&lt;p&gt;My approach was to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set conservative per-domain request limits.&lt;/li&gt;
&lt;li&gt;Add delays or rate limiting where appropriate.&lt;/li&gt;
&lt;li&gt;Reduce request pressure after repeated 429 responses.&lt;/li&gt;
&lt;li&gt;Use exponential backoff for temporary failures.&lt;/li&gt;
&lt;li&gt;Avoid launching duplicate jobs for the same URLs.&lt;/li&gt;
&lt;li&gt;Increase concurrency only after measuring the effect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The appropriate limit depends on the target site's rules, response times, and the scale of the project. There isn't one universal request-per-second value that works everywhere.&lt;/p&gt;

&lt;p&gt;I also checked the site's terms and applicable crawling policies before automating requests. I preferred official APIs or bulk data exports when they provided the information I needed. I didn't design the scraper to bypass authentication, CAPTCHAs, or access controls.&lt;/p&gt;

&lt;p&gt;The goal wasn't to extract the maximum number of pages in the shortest time. It was to finish reliably without creating unnecessary load or getting the job blocked.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. I Added Logging and Monitoring That Answered Useful Questions
&lt;/h2&gt;

&lt;p&gt;A scraper can fail in ways that are difficult to reproduce. Without useful logs, debugging becomes a guessing game.&lt;/p&gt;

&lt;p&gt;I moved beyond generic messages such as Request failed and recorded enough context to identify the problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URL or internal job ID&lt;/li&gt;
&lt;li&gt;HTTP status code&lt;/li&gt;
&lt;li&gt;Attempt number&lt;/li&gt;
&lt;li&gt;Request duration&lt;/li&gt;
&lt;li&gt;Error category&lt;/li&gt;
&lt;li&gt;Parsing or validation result&lt;/li&gt;
&lt;li&gt;Number of records stored&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I avoided logging credentials, session cookies, authorization headers, or other sensitive information.&lt;/p&gt;

&lt;p&gt;I also added run-level metrics. At the end of each run, I wanted to know how many pages were discovered, attempted, completed, retried, skipped, and permanently failed.&lt;/p&gt;

&lt;p&gt;That helped answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did the target website become unavailable?&lt;/li&gt;
&lt;li&gt;Did the scraper encounter a rate limit?&lt;/li&gt;
&lt;li&gt;Did the HTML change?&lt;/li&gt;
&lt;li&gt;Did the parser return empty records?&lt;/li&gt;
&lt;li&gt;Did the database reject writes?&lt;/li&gt;
&lt;li&gt;Did the job finish with unusually little data?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used alerts for meaningful changes rather than every isolated error. A single failed page may not matter, but a sudden spike in failed requests or a major drop in valid records deserves attention.&lt;/p&gt;

&lt;p&gt;Monitoring also gave me a baseline. Once I understood the scraper's normal success rate and execution time, unusual behavior became much easier to spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. I Tested Failure Scenarios, Not Just the Happy Path
&lt;/h2&gt;

&lt;p&gt;Testing a scraper with one working page doesn't tell you much about how it behaves in production.&lt;/p&gt;

&lt;p&gt;I started testing cases that could realistically break it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A request times out.&lt;/li&gt;
&lt;li&gt;The server returns HTTP 429 or 503.&lt;/li&gt;
&lt;li&gt;A page returns HTML without the expected title.&lt;/li&gt;
&lt;li&gt;An optional field is missing.&lt;/li&gt;
&lt;li&gt;The same URL is processed twice.&lt;/li&gt;
&lt;li&gt;The database becomes temporarily unavailable.&lt;/li&gt;
&lt;li&gt;A job stops halfway through a batch.&lt;/li&gt;
&lt;li&gt;A malformed page contains unexpected characters or empty values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I separated parser tests from network tests. Parser tests used saved HTML, which made them quick and deterministic. Network tests checked the request layer separately, with controlled timeouts and mocked responses where appropriate.&lt;/p&gt;

&lt;p&gt;I also tested recovery. If a run stopped after processing half the URLs, could it resume without losing work or creating duplicates? If the parser started failing across many pages, would the monitoring reveal the problem?&lt;/p&gt;

&lt;p&gt;Those tests were more valuable than simply confirming that the scraper worked once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture I Would Use for a Reliable Scraper
&lt;/h2&gt;

&lt;p&gt;For a small or medium-sized scraping project, I would start with this design:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;URL Source
    |
    v
Job Queue or URL Table
    |
    v
Request Layer
    |
    v
HTML Parser
    |
    v
Data Validation
    |
    v
Database / Data Store
    |
    v
Logs, Metrics, and Alerts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each stage has a clear responsibility. The request layer handles network behavior, the parser extracts fields, validation checks data quality, and storage manages persistence and duplicates.&lt;/p&gt;

&lt;p&gt;For a small project, a single Python process and a database-backed URL table may be sufficient. For a larger workload, I might introduce a queue, multiple workers, a separate monitoring service, and a dedicated scheduler.&lt;/p&gt;

&lt;p&gt;I wouldn't add distributed infrastructure just because it sounds production-ready. Extra components create their own operational problems. The architecture should match the volume of data, acceptable failure rate, recovery requirements, and maintenance budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned From Building It
&lt;/h2&gt;

&lt;p&gt;The biggest lesson was that a reliable web scraper is not defined by how quickly it extracts a page. It's defined by what happens when the page is unavailable, the markup changes, the database rejects a write, or the job stops unexpectedly.&lt;/p&gt;

&lt;p&gt;If I were starting another scraper today, I would prioritize these five things from the beginning:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Explicit timeouts and bounded retries to handle temporary network problems.&lt;/li&gt;
&lt;li&gt;Data validation to catch broken extraction before it reaches the database.&lt;/li&gt;
&lt;li&gt;Persistent job state to resume interrupted work safely.&lt;/li&gt;
&lt;li&gt;Idempotent storage to prevent duplicate records during retries and restarts.&lt;/li&gt;
&lt;li&gt;Monitoring and parser tests to detect changes before they cause widespread data loss.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I would add more workers or more complex infrastructure only after the basic system was reliable.&lt;/p&gt;

&lt;p&gt;A scraper doesn't need to be complicated to be production-ready. It needs predictable behavior, clear failure states, useful logs, and a way to recover when things go wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building a web scraper that survives production taught me to think beyond requests and selectors. The real work was handling uncertainty: websites that change, servers that fail, data that arrives incomplete, and jobs that stop halfway through. By separating the request, parsing, validation, and storage layers, adding bounded retries, tracking progress, and monitoring data quality, I could make the scraper much easier to maintain and recover.&lt;/p&gt;

&lt;p&gt;The goal isn't to build a scraper that never fails. That's unrealistic. The goal is to build one that detects failures, avoids corrupting data, recovers safely, and tells you when something needs attention. That's the difference between a script that works during a demo and a scraper you can trust in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. How do you make a web scraper reliable in production?
&lt;/h3&gt;

&lt;p&gt;Use request timeouts, bounded retries, rate limiting, data validation, persistent job state, idempotent database writes, logging, and automated tests. Monitor extraction quality as well as request success so that structural changes don't silently corrupt your dataset.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Which Python libraries are best for building a web scraper?
&lt;/h3&gt;

&lt;p&gt;requests is a good option for straightforward HTTP requests, while Beautiful Soup is useful for parsing HTML. Scrapy is designed for larger crawling workflows with built-in scheduling and concurrency controls. Playwright can handle pages that depend on browser-side JavaScript when ordinary HTTP requests aren't sufficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. How do you handle website layout changes in a scraper?
&lt;/h3&gt;

&lt;p&gt;Keep selectors maintainable, test parsers against saved HTML samples, validate required fields, and monitor extraction success rates. Alert when important fields suddenly become missing or the number of valid records drops unexpectedly.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. How can you prevent duplicate data when a scraper retries?
&lt;/h3&gt;

&lt;p&gt;Use a stable unique identifier, enforce uniqueness in the database, and use upsert operations instead of blindly inserting every result. Keep processing state in persistent storage so the scraper can resume after an interruption.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Should a web scraper use multiple threads or workers?
&lt;/h3&gt;

&lt;p&gt;It depends on the workload. Concurrency can improve throughput for network-bound scraping, but too many simultaneous requests may trigger rate limits or overload the target site. Start with conservative limits, respect the site's policies, and increase concurrency only after measuring reliability and performance.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>productivity</category>
      <category>devdiscuss</category>
      <category>discuss</category>
    </item>
    <item>
      <title>This is actually such a fun idea 😂 I definitely want to see what kind of setups people are going to share.</title>
      <dc:creator>Alina Shah</dc:creator>
      <pubDate>Thu, 08 Oct 2026 18:49:37 +0000</pubDate>
      <link>https://dev.to/alinashah/this-is-actually-such-a-fun-idea-i-definitely-want-to-see-what-kind-of-setups-people-are-going-to-1555</link>
      <guid>https://dev.to/alinashah/this-is-actually-such-a-fun-idea-i-definitely-want-to-see-what-kind-of-setups-people-are-going-to-1555</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/devstackhub/show-me-your-dev-setup-ill-roast-it-3d5b" class="crayons-story__hidden-navigation-link"&gt;Show Me Your Dev Setup — I'll Roast It 🖥️🔥&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/devstackhub/show-me-your-dev-setup-ill-roast-it-3d5b" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Almost everyone uses a dark theme editor&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/devstackhub" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4081963%2F9d10c5fa-c76e-4c9d-88af-e3f2fced5a62.jpg" alt="devstackhub profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/devstackhub" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Dev Stack Hub
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Dev Stack Hub
                
                
              
              &lt;div id="story-author-preview-content-4819543" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/devstackhub" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4081963%2F9d10c5fa-c76e-4c9d-88af-e3f2fced5a62.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Dev Stack Hub&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/devstackhub/show-me-your-dev-setup-ill-roast-it-3d5b" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 8&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/devstackhub/show-me-your-dev-setup-ill-roast-it-3d5b" id="article-link-4819543"&gt;
          Show Me Your Dev Setup — I'll Roast It 🖥️🔥
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/discuss"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;discuss&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devdiscuss"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devdiscuss&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/showdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;showdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/community"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;community&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/devstackhub/show-me-your-dev-setup-ill-roast-it-3d5b" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;47&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/devstackhub/show-me-your-dev-setup-ill-roast-it-3d5b#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              55&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>programming</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
