{"id":31646,"date":"2025-01-16T18:35:27","date_gmt":"2025-01-16T18:35:27","guid":{"rendered":"https:\/\/peraltafinancing.com\/analytics\/dynamic-web-scraping-with-python-pandas-and-duckdb\/"},"modified":"2025-01-16T18:35:27","modified_gmt":"2025-01-16T18:35:27","slug":"dynamic-web-scraping-with-python-pandas-and-duckdb","status":"publish","type":"post","link":"https:\/\/fivemor.com\/?p=31646","title":{"rendered":"Dynamic Web Scraping with Python, Pandas and DuckDB"},"content":{"rendered":"<p> <br \/>\n<\/p>\n<div>\n<div data-breakout=\"normal\">\n<div class=\"iwMEp\" id=\"viewer-l3ft1248\">\n<div class=\"rxF05 qTNqm\">\n<figure class=\"XObdu\">\n<div role=\"button\" data-hook=\"imageViewer\" tabindex=\"0\" class=\"vhG7C\" aria-haspopup=\"https:\/\/www.marqeu.com\/true\">\n<div style=\"--dim-height:808;--dim-width:1436\" id=\"l3ft1248\" class=\"L2P8r Ku1Gj yS7zq\"><wow-image id=\"a64899_ea1222b1658f459f972268b431ac2386~mv2.png\" class=\"undefined lIVgj\" data-image-info=\"{&quot;containerId&quot;:&quot;l3ft1248&quot;,&quot;displayMode&quot;:&quot;fill&quot;,&quot;isLQIP&quot;:true,&quot;isSEOBot&quot;:false,&quot;lqipTransition&quot;:&quot;blur&quot;,&quot;imageData&quot;:{&quot;width&quot;:1436,&quot;height&quot;:808,&quot;uri&quot;:&quot;a64899_ea1222b1658f459f972268b431ac2386~mv2.png&quot;,&quot;name&quot;:&quot;&quot;,&quot;displayMode&quot;:&quot;fill&quot;}}\" data-motion-part=\"BG_IMG\" data-bg-effect-name=\"\" data-has-ssr-src=\"https:\/\/www.marqeu.com\/true\" data-animate-blur=\"\"><img decoding=\"async\" alt=\"\" style=\"width:100%;height:100%;object-fit:cover;object-position:50% 50%;max-width:100%\" src=\"https:\/\/www.marqeu.com\/python-web-scraping\" data-pin-media=\"https:\/\/static.wixstatic.com\/media\/a64899_ea1222b1658f459f972268b431ac2386~mv2.png\/v1\/fill\/w_1436,h_808,al_c,q_90\/a64899_ea1222b1658f459f972268b431ac2386~mv2.png\" draggable=\"false\"\/><\/wow-image><\/div>\n<\/div>\n<\/figure>\n<\/div>\n<\/div>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-7kj40\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span><span>Ever felt the frustration of wrangling with mountains of data through web scraping? The landscape of marketing technology is constantly growing and <\/span><a class=\"OzMgK\" href=\"https:\/\/www.marqeu.com\/blog\/hashtags\/marketing\" target=\"__blank\"><span>#marketing<\/span><\/a><span> teams are experimenting with new tools that are constantly generating a tons of data. As <\/span><\/span><\/span><a target=\"_self\" href=\"https:\/\/marqeu.com\/marketing-analytics\/\" rel=\"noopener\" class=\"l04dV ro37v\" data-hook=\"WebLink\"><span style=\"font-size:17px\"><span>#MarketingAnalytics professionals<\/span><\/span><\/a><span style=\"font-size:17px\"><span>, we constantly juggle data from such tools and sometimes, API connectors just aren\u2019t available ready-made from tools like Fivetran or Airbyte. In such cases, we have to rely on building a custom Python scripts to either leverage the API of that data source (which is relatively an easier option) or scrape the authenticated web pages with the help of Python libraries like BeautifulSoup and Selenium.<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<div style=\"display:flex;justify-content:flex-start\" dir=\"ltr\" id=\"viewer-fmisf\">\n<blockquote class=\"uNGkF\"><p><span><\/p>\n<p><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>Python based web scraping can be a real beast to tame, especially when dealing with massive datasets and dynamic web pages.<\/span><\/span><\/span><\/p>\n<p><\/span><\/p><\/blockquote>\n<\/div>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-5g421\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>Join me on a journey where I harness the power of Python, Pandas, DuckDB, and web scraping to not just access data but to convert it into actionable data model that powers a very <\/span><\/span><a target=\"_self\" href=\"https:\/\/marqeu.com\/nurture\/\" rel=\"noopener\" class=\"l04dV ro37v\" data-hook=\"WebLink\"><span style=\"font-size:17px\"><span>targeted marketing engagement strategy<\/span><\/span><\/a><span style=\"font-size:17px\"><span>.<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-6mdfs\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>\ud83d\udd78\ufe0f <\/span><\/span><strong style=\"font-weight:700\"><span style=\"font-size:17px\"><span>The Challenge: Scraping a CMS Blog without an API<\/span><\/span><\/strong><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-6nruf\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>I recently faced this exact dilemma: scraping blog data from our CMS, which lacked an API. My trusty Python script, armed with BeautifulSoup (BS4) and Selenium, was ready for battle. Tackling the dynamic intricacies of web pages, especially in the context of pagination and the dynamic loading of client-side content (thanks to AJAX and JavaScript), posed a distinctive set of hurdles.<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-3d8ng\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>\ud83d\udee0\ufe0f <\/span><\/span><strong style=\"font-weight:700\"><span style=\"font-size:17px\"><span>Configuring UserAgent, Sessions, TLS, Referrer, and Chrome options<\/span><\/span><\/strong><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-a56tn\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>Setting up the basic script was a smooth sailing. The excitement kicked in when I started exploring the DOM structure of the web pages, pinpointing the specific HTML tag that housed the data I needed for this project.<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-dbbn\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>The sight when any of my Python scripts first render web scraped data as list item is always such a joy and never gets old. It\u2019s that satisfying feeling of problem solved!<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<pre class=\"uGcvR _3w1DA WzoeH\" dir=\"auto\" id=\"viewer-fanog\"><span class=\"\"><span>driver.find_element(By.TAG_NAME, 'tbody').find_elements(By.CLASS_NAME, 'profile_groups')\n\nload_more = driver.find_element(By.XPATH,  \n                                '\/\/*[@id=\"_profilegroup\"]\/div[2]\/div\/div[2]\/div\/div\/button')  \nload_more.click()<\/span><\/span><\/pre>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-147a8\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>\ud83d\udd75\ufe0f <\/span><\/span><strong style=\"font-weight:700\"><span style=\"font-size:17px\"><span>The Webpage DOM and the \u201cLoad More\u201d Conundrum<\/span><\/span><\/strong><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-78egb\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>In this case though, the joy of seeing the first set of rows loaded was short-lived! Things got interesting as I hunted down the elusive \u201cLoad More\u201d button, aiming to grab additional rows dynamically before I can read and store the data in a Pandas DataFrame.<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-7lj4q\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>Attempting to load all the data in one go turned out to be a problem, causing memory overload and web page crashes. With thousands of rows, my script started to buckle under the weight of the data.<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-8890l\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>Clearly this option of first loading all the data on the webpage and then scraping was not going to work. Traditional methods of storing everything in a Pandas DataFrame after full scraping is done or directly writing to a database like Snowflake were simply too resource-intensive.<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-3epe5\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>\ud83e\udd86<\/span><\/span><strong style=\"font-weight:700\"><span style=\"font-size:17px\"><span> DuckDB comes to rescue with Plan B<\/span><\/span><\/strong><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-600kd\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>So, for my Plan B, I devised a strategy to leverage DuckDB. First, I would load the initial batch of rows, scrape the data, and then stash it away in storage. After that, I\u2019d load the next batch of rows on the webpage and repeat these steps, employing 2 loops. The key differentiator in this approach was to use Pandas DataFrame with a companion, DuckDB for the iterative inserts and continuous updates.<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-389n4\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>To handle 2 different loop iterations in resonance, I had to come up with an interesting algorithm because one of the loops in my Python script was running on page load sequence but with every \u201cLoad More\u201d click, the secondary loop was loading additional rows. These added rows were stitched onto the ongoing sequence from the very start of the webpage load, not kicking off a fresh sequence. While one loop had to kick off anew, the other needed to pick up right where the first loop had left off.<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-ahfs3\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>Wrapping my head around and implementing this puzzle piece turned out to be quite a delightful journey!<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<div style=\"display:flex;justify-content:flex-start\" dir=\"ltr\" id=\"viewer-c2bs8\">\n<blockquote class=\"uNGkF\"><p><span><\/p>\n<p><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>The mighty \u2018f\u2019 strings came to the rescue here and I could make the XPATH of the key elements dynamic \u2013 \u201ctr[{counter + 1}]\u201d.<\/span><\/span><\/span><\/p>\n<p><\/span><\/p><\/blockquote>\n<\/div>\n<\/div>\n<div data-breakout=\"normal\">\n<pre class=\"uGcvR _3w1DA WzoeH\" dir=\"auto\" id=\"viewer-cpgg1\"><span class=\"\"><span>if profiles:  \n    if len(profiles[starting_point:ending_point]) &gt; 0:  \n        logging.info(f'beginning extraction of profile data for iteration no: {i}')  \n        for item in profiles[starting_point:ending_point]:   \n  \n            # company  \n            try:  \n                item.find_elements(By.XPATH,  \n                                   f'\/\/*[@id=\"_profile\"]\/div[2]\/div\/div\/div\/table\/tbody\/tr[{counter + 1}]')  \n            except NoSuchElementException as err:  \n                logging.info(f'could not locate element for company on iteration no. {i}')  \n                company = ''  \n            else:  \n                company = item.find_element(By.XPATH,  \n                                            f'\/\/*[@id=\"_profile\"]\/div[2]\/div[2]\/div\/table\/tbody\/tr[{counter + 1}]').text  \n  \n            # domain  \n            try:  \n                item.find_element(By.XPATH,  \n                                  f'\/\/*[@id=\"_profile\"]\/div[2]\/div[2]\/div\/div\/table\/tbody\/tr[{counter + 1}]') \n            except NoSuchElementException as err:  \n                logging.info(f'could not locate element for domain on iteration no. {i}')  \n                domain = ''  \n            else:  \n                domain = item.find_element(By.XPATH,  \n                                           f'\/\/*[@id=\"_profile\"]\/div[2]\/div[2]\/div\/table\/tbody\/tr[{counter + 1}]').text<\/span><\/span><\/pre>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-11th6\"><span class=\"lMv7L dknLS\"><span>This kind of algorithm and data volumes involve some serious input\/output (I\/O) action. Using just Pandas DataFrame to store this data incrementally in a CSV file or a database like Snowflake would burn a hole in my resources.<\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-48nfh\"><span class=\"lMv7L dknLS\"><span>You see, dynamic and ongoing web scraping Python scripts have a tendency to get shaky, especially when faced with webpage refreshes and shaky internet connections. Given the hefty load of data and the repetitive looping through the loaded rows, I needed a solution to effortlessly fetch and store the data one iteration at a time, considering I had over 7500 iterations on my hands. And here\u2019s where DuckDB stepped in!<\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-3q1e8\"><span class=\"lMv7L dknLS\"><strong style=\"font-weight:700\"><em style=\"font-style:italic\"><span>\ud83d\ude80 <\/span><\/em><\/strong><strong style=\"font-weight:700\"><span>Harnessing DuckDB for Stability in Python-Based Web Scraping<\/span><\/strong><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-cgort\"><span class=\"lMv7L dknLS\"><span>As part of my script, I initialized a DuckDB database and created a table with the relevant columns. Once a selenium iteration (as shown above) was finished successfully, I loaded the set of rows scraped into a DataFrame via a list of dictionaries. Once the DataFrame was built, I then inserted all the rows in the DataFrame into DuckDB table and clear out the DataFrame for next set of rows being scraped by Selenium Python script.<\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-25sn1\"><span class=\"lMv7L dknLS\"><span>The primary objective of using this approach was to avoid the instability associated with Selenium scripts when it comes to scraping a massive database from a dynamic (AJAX \/ JavaScript) based web page and avoid Snowflake credit usage associated with such I\/O operations during the development cycle.<\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<pre class=\"uGcvR _3w1DA WzoeH\" dir=\"auto\" id=\"viewer-dud3q\"><span class=\"\"><span>if company != '':  \n  \n                try:  \n                    str(company).strip()  \n                except:  \n                    logging.info(f'company name clean-up not possible for iteration {i} and {counter}')  \n                else:  \n                    company = str(company).strip()  \n                try:  \n                    str(domain).strip()  \n                except:  \n                    logging.info(f'domain name clean-up not possible for iteration {i} and {counter}')  \n                else:  \n                    domain = str(domain).strip()  \n                try:  \n                    str(title).strip()  \n                except:  \n                    logging.info(f'title clean-up not possible for iteration {i} and {counter}')  \n                else:  \n                    title = str(title).strip()  \n  \n                extract.append(  \n                    {      \n                        'company': company,  \n                        'title': title,  \n                        'domain': domain  \n                    }  \n                )  \n  \n                logging.info(f'successfully extracted data for profile no: {counter + 1}')  \n  \n            counter += 1  \n            logging.info(f'successfully finished extraction for all profiles data for iteration no: {i}')  \n    else:  \n        logging.info(f'no profiles found in the iteration no: {i}')  \n  \n    if len(contact_extract) &gt; 0:  \n        df = pandas.DataFrame(data=extract)  \n  \n        load_duckdb(df)  \n  \n        logging.info(f'successfully loaded set no: {i} into duckDB.')<\/span><\/span><\/pre>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-28c74\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>In the realm of such Python web scraping scripts that stretch over hours,<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<div style=\"display:flex;justify-content:flex-start\" dir=\"ltr\" id=\"viewer-7a7f5\">\n<blockquote class=\"uNGkF\"><p><span><\/p>\n<p><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>custom Python logs emerged as unsung heroes, not only in keeping tabs on the script\u2019s ongoing status but also proving handy when things go south,<\/span><\/span><\/span><\/p>\n<p><\/span><\/p><\/blockquote>\n<\/div>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-7lihq\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>and Python scripts decide to play hide and seek with errors. In a project-first, I opted to store these logs in a separate JSON file, just for the fun of it.<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-foe15\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>If only, I could do the same with SQL CTEs!<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-9hhmh\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>\ud83e\uddf9 <\/span><\/span><strong style=\"font-weight:700\"><span style=\"font-size:17px\"><span>Seamless Data Transition and Cleanup in Pandas<\/span><\/span><\/strong><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<div style=\"display:flex;justify-content:flex-start\" dir=\"ltr\" id=\"viewer-7jv53\">\n<blockquote class=\"uNGkF\"><p><span><\/p>\n<p><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>After finishing up the scraping process, I effortlessly brought all the data back from DuckDB into a DataFrame with just one line of code.<\/span><\/span><\/span><\/p>\n<p><\/span><\/p><\/blockquote>\n<\/div>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-dqjsl\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>From there, it was business as usual, employing the familiar tools of Pandas to tidy up and normalize the data. While I would like to handle all the data cleanup and normalization within DuckDB (or Polars for that matter) someday, I\u2019m currently relishing the benefits of blending the best of both worlds.<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-2ki0r\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>\ud83d\udcb0 <\/span><\/span><strong style=\"font-weight:700\"><span style=\"font-size:17px\"><span>Optimizing Snowflake Costs with Python-DuckDB Alchemy<\/span><\/span><\/strong><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-9ak2e\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>After the data was transformed, I turned to SQLAlchemy to seamlessly dispatch it to a Snowflake table. Once settled in Snowflake, the next step involved<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<div style=\"display:flex;justify-content:flex-start\" dir=\"ltr\" id=\"viewer-38har\">\n<blockquote class=\"uNGkF\"><p><span><\/p>\n<p><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>transferring this dataset to Marketo, a move facilitated by a Reverse ETL process utilizing Census.<\/span><\/span><\/span><\/p>\n<p><\/span><\/p><\/blockquote>\n<\/div>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-1ud6h\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>Throughout this entire process, especially in the development cycle, it marked the first instance where I had to tap into Snowflake credits.<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-ct29l\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>\ud83c\udf10 <\/span><\/span><strong style=\"font-weight:700\"><span style=\"font-size:17px\"><span>The Power and Promise of DuckDB<\/span><\/span><\/strong><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-e2nc2\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>Cost optimization is currently a key focus for every organization when it comes to Snowflake, and instances like these showcase how Python, coupled with the prowess of an OLAP database like DuckDB (harnessing the unlimited power of M2 Macs), can play a vital role. While I had been casually experimenting with DuckDB for some random testing use cases,<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<div style=\"display:flex;justify-content:flex-start\" dir=\"ltr\" id=\"viewer-237pa\">\n<blockquote class=\"uNGkF\"><p><span><\/p>\n<p><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>DuckDB\u2019s robustness, simplicity, and particularly its Python API and integration with Pandas blew me away.<\/span><\/span><\/span><\/p>\n<p><\/span><\/p><\/blockquote>\n<\/div>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-csorc\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>This implementation served as my inaugural semi-professional venture with DuckDB, as it\u2019s not a script in constant motion via AirFlow or Prefect. Excited to delve deeper into leveraging DuckDB for more such use cases involving extensive development iterations and massive data I\/O operations.<\/span><\/span><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-78la1\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span>\ud83e\udd1d <\/span><\/span><strong style=\"font-weight:700\"><span style=\"font-size:17px\"><span>Join the Conversation<\/span><\/span><\/strong><\/span><\/p>\n<\/div>\n<div data-breakout=\"normal\">\n<p class=\"a4yOF NlrTp dknLS SeNKp\" dir=\"auto\" id=\"viewer-43i27\"><span class=\"lMv7L dknLS\"><span style=\"font-size:17px\"><span><span>Have a similar use case or challenges to share? I\u2019m eager to dive into new data frontiers. Let\u2019s exchange insights and push the boundaries of what Python, Pandas, and DuckDB can achieve in the dynamic landscape of <\/span><a class=\"OzMgK\" href=\"https:\/\/www.marqeu.com\/blog\/hashtags\/MarketingAnalytics\" target=\"__blank\"><span>#MarketingAnalytics<\/span><\/a><span>.<\/span><\/span><\/span><\/span><\/p>\n<\/div>\n<\/div>\n\n","protected":false},"excerpt":{"rendered":"<p>Ever felt the frustration of wrangling with mountains of data through web scraping? The landscape of marketing technology is constantly growing and #marketing teams are experimenting with new tools that are constantly generating a tons of data. As #MarketingAnalytics professionals, we constantly juggle data from such tools and sometimes, API connectors just aren\u2019t available ready-made [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":31647,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[12033],"tags":[21304,15321,21303,21302,21301,5393],"dealstore":[],"offerexpiration":[],"class_list":["post-31646","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-analytics","tag-duckdb","tag-dynamic","tag-pandas","tag-python","tag-scraping","tag-web"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.4 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Dynamic Web Scraping with Python, Pandas and DuckDB - Som2ny Network<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/fivemor.com\/?p=31646\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Dynamic Web Scraping with Python, Pandas and DuckDB - Som2ny Network\" \/>\n<meta property=\"og:description\" content=\"Ever felt the frustration of wrangling with mountains of data through web scraping? The landscape of marketing technology is constantly growing and #marketing teams are experimenting with new tools that are constantly generating a tons of data. As #MarketingAnalytics professionals, we constantly juggle data from such tools and sometimes, API connectors just aren\u2019t available ready-made [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/fivemor.com\/?p=31646\" \/>\n<meta property=\"og:site_name\" content=\"Som2ny Network\" \/>\n<meta property=\"article:published_time\" content=\"2025-01-16T18:35:27+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/01\/a64899_ea1222b1658f459f972268b431ac2386mv2.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1000\" \/>\n\t<meta property=\"og:image:height\" content=\"563\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"admin\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"admin\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"8 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/fivemor.com\/?p=31646#article\",\"isPartOf\":{\"@id\":\"https:\/\/fivemor.com\/?p=31646\"},\"author\":{\"name\":\"admin\",\"@id\":\"https:\/\/fivemor.com\/#\/schema\/person\/b85e3c3dc0e1daea076524dc8810c371\"},\"headline\":\"Dynamic Web Scraping with Python, Pandas and DuckDB\",\"datePublished\":\"2025-01-16T18:35:27+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/fivemor.com\/?p=31646\"},\"wordCount\":1309,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/fivemor.com\/#organization\"},\"image\":{\"@id\":\"https:\/\/fivemor.com\/?p=31646#primaryimage\"},\"thumbnailUrl\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/01\/a64899_ea1222b1658f459f972268b431ac2386mv2.png\",\"keywords\":[\"DuckDB\",\"Dynamic\",\"Pandas\",\"Python\",\"Scraping\",\"Web\"],\"articleSection\":[\"Analytics\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/fivemor.com\/?p=31646#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/fivemor.com\/?p=31646\",\"url\":\"https:\/\/fivemor.com\/?p=31646\",\"name\":\"Dynamic Web Scraping with Python, Pandas and DuckDB - Som2ny Network\",\"isPartOf\":{\"@id\":\"https:\/\/fivemor.com\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/fivemor.com\/?p=31646#primaryimage\"},\"image\":{\"@id\":\"https:\/\/fivemor.com\/?p=31646#primaryimage\"},\"thumbnailUrl\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/01\/a64899_ea1222b1658f459f972268b431ac2386mv2.png\",\"datePublished\":\"2025-01-16T18:35:27+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/fivemor.com\/?p=31646#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/fivemor.com\/?p=31646\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/fivemor.com\/?p=31646#primaryimage\",\"url\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/01\/a64899_ea1222b1658f459f972268b431ac2386mv2.png\",\"contentUrl\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/01\/a64899_ea1222b1658f459f972268b431ac2386mv2.png\",\"width\":1000,\"height\":563},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/fivemor.com\/?p=31646#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/fivemor.com\/?bp_activities=1\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Dynamic Web Scraping with Python, Pandas and DuckDB\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/fivemor.com\/#website\",\"url\":\"https:\/\/fivemor.com\/\",\"name\":\"Som2ny Network\",\"description\":\"Daily Deals\",\"publisher\":{\"@id\":\"https:\/\/fivemor.com\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/fivemor.com\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/fivemor.com\/#organization\",\"name\":\"Som2ny Network\",\"url\":\"https:\/\/fivemor.com\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/fivemor.com\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2026\/07\/4a0953c4-logo-300x86-1.png\",\"contentUrl\":\"https:\/\/fivemor.com\/wp-content\/uploads\/2026\/07\/4a0953c4-logo-300x86-1.png\",\"width\":300,\"height\":86,\"caption\":\"Som2ny Network\"},\"image\":{\"@id\":\"https:\/\/fivemor.com\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\/\/fivemor.com\/#\/schema\/person\/b85e3c3dc0e1daea076524dc8810c371\",\"name\":\"admin\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/fivemor.com\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/729ae85bf62b9917e93538db2f2688ca?s=96&r=g&default=https%3A%2F%2Ffivemor.com%2Fwp-content%2Fplugins%2Fbuddypress-first-letter-avatar%2Fimages%2Fdefault%2F96%2Flatin_a.png\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/729ae85bf62b9917e93538db2f2688ca?s=96&r=g&default=https%3A%2F%2Ffivemor.com%2Fwp-content%2Fplugins%2Fbuddypress-first-letter-avatar%2Fimages%2Fdefault%2F96%2Flatin_a.png\",\"caption\":\"admin\"},\"sameAs\":[\"https:\/\/fivemor.com\"],\"url\":\"https:\/\/fivemor.com\/?author=1\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Dynamic Web Scraping with Python, Pandas and DuckDB - Som2ny Network","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/fivemor.com\/?p=31646","og_locale":"en_US","og_type":"article","og_title":"Dynamic Web Scraping with Python, Pandas and DuckDB - Som2ny Network","og_description":"Ever felt the frustration of wrangling with mountains of data through web scraping? The landscape of marketing technology is constantly growing and #marketing teams are experimenting with new tools that are constantly generating a tons of data. As #MarketingAnalytics professionals, we constantly juggle data from such tools and sometimes, API connectors just aren\u2019t available ready-made [&hellip;]","og_url":"https:\/\/fivemor.com\/?p=31646","og_site_name":"Som2ny Network","article_published_time":"2025-01-16T18:35:27+00:00","og_image":[{"width":1000,"height":563,"url":"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/01\/a64899_ea1222b1658f459f972268b431ac2386mv2.png","type":"image\/png"}],"author":"admin","twitter_card":"summary_large_image","twitter_misc":{"Written by":"admin","Est. reading time":"8 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/fivemor.com\/?p=31646#article","isPartOf":{"@id":"https:\/\/fivemor.com\/?p=31646"},"author":{"name":"admin","@id":"https:\/\/fivemor.com\/#\/schema\/person\/b85e3c3dc0e1daea076524dc8810c371"},"headline":"Dynamic Web Scraping with Python, Pandas and DuckDB","datePublished":"2025-01-16T18:35:27+00:00","mainEntityOfPage":{"@id":"https:\/\/fivemor.com\/?p=31646"},"wordCount":1309,"commentCount":0,"publisher":{"@id":"https:\/\/fivemor.com\/#organization"},"image":{"@id":"https:\/\/fivemor.com\/?p=31646#primaryimage"},"thumbnailUrl":"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/01\/a64899_ea1222b1658f459f972268b431ac2386mv2.png","keywords":["DuckDB","Dynamic","Pandas","Python","Scraping","Web"],"articleSection":["Analytics"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/fivemor.com\/?p=31646#respond"]}]},{"@type":"WebPage","@id":"https:\/\/fivemor.com\/?p=31646","url":"https:\/\/fivemor.com\/?p=31646","name":"Dynamic Web Scraping with Python, Pandas and DuckDB - Som2ny Network","isPartOf":{"@id":"https:\/\/fivemor.com\/#website"},"primaryImageOfPage":{"@id":"https:\/\/fivemor.com\/?p=31646#primaryimage"},"image":{"@id":"https:\/\/fivemor.com\/?p=31646#primaryimage"},"thumbnailUrl":"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/01\/a64899_ea1222b1658f459f972268b431ac2386mv2.png","datePublished":"2025-01-16T18:35:27+00:00","breadcrumb":{"@id":"https:\/\/fivemor.com\/?p=31646#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/fivemor.com\/?p=31646"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/fivemor.com\/?p=31646#primaryimage","url":"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/01\/a64899_ea1222b1658f459f972268b431ac2386mv2.png","contentUrl":"https:\/\/fivemor.com\/wp-content\/uploads\/2025\/01\/a64899_ea1222b1658f459f972268b431ac2386mv2.png","width":1000,"height":563},{"@type":"BreadcrumbList","@id":"https:\/\/fivemor.com\/?p=31646#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/fivemor.com\/?bp_activities=1"},{"@type":"ListItem","position":2,"name":"Dynamic Web Scraping with Python, Pandas and DuckDB"}]},{"@type":"WebSite","@id":"https:\/\/fivemor.com\/#website","url":"https:\/\/fivemor.com\/","name":"Som2ny Network","description":"Daily Deals","publisher":{"@id":"https:\/\/fivemor.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/fivemor.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/fivemor.com\/#organization","name":"Som2ny Network","url":"https:\/\/fivemor.com\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/fivemor.com\/#\/schema\/logo\/image\/","url":"https:\/\/fivemor.com\/wp-content\/uploads\/2026\/07\/4a0953c4-logo-300x86-1.png","contentUrl":"https:\/\/fivemor.com\/wp-content\/uploads\/2026\/07\/4a0953c4-logo-300x86-1.png","width":300,"height":86,"caption":"Som2ny Network"},"image":{"@id":"https:\/\/fivemor.com\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/fivemor.com\/#\/schema\/person\/b85e3c3dc0e1daea076524dc8810c371","name":"admin","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/fivemor.com\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/729ae85bf62b9917e93538db2f2688ca?s=96&r=g&default=https%3A%2F%2Ffivemor.com%2Fwp-content%2Fplugins%2Fbuddypress-first-letter-avatar%2Fimages%2Fdefault%2F96%2Flatin_a.png","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/729ae85bf62b9917e93538db2f2688ca?s=96&r=g&default=https%3A%2F%2Ffivemor.com%2Fwp-content%2Fplugins%2Fbuddypress-first-letter-avatar%2Fimages%2Fdefault%2F96%2Flatin_a.png","caption":"admin"},"sameAs":["https:\/\/fivemor.com"],"url":"https:\/\/fivemor.com\/?author=1"}]}},"_links":{"self":[{"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/posts\/31646","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=31646"}],"version-history":[{"count":0,"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/posts\/31646\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=\/wp\/v2\/media\/31647"}],"wp:attachment":[{"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=31646"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=31646"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=31646"},{"taxonomy":"dealstore","embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fdealstore&post=31646"},{"taxonomy":"offerexpiration","embeddable":true,"href":"https:\/\/fivemor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fofferexpiration&post=31646"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}