<?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: Kang Jeongsik</title>
    <description>The latest articles on DEV Community by Kang Jeongsik (@vintage97).</description>
    <link>https://dev.to/vintage97</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%2F3941457%2Fe484fd93-9132-442c-90e2-2dc5686225df.png</url>
      <title>DEV Community: Kang Jeongsik</title>
      <link>https://dev.to/vintage97</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vintage97"/>
    <language>en</language>
    <item>
      <title>How to respect robots.txt and Crawl-delay in Python</title>
      <dc:creator>Kang Jeongsik</dc:creator>
      <pubDate>Fri, 16 Oct 2026 05:15:24 +0000</pubDate>
      <link>https://dev.to/vintage97/how-to-respect-robotstxt-and-crawl-delay-in-python-5d22</link>
      <guid>https://dev.to/vintage97/how-to-respect-robotstxt-and-crawl-delay-in-python-5d22</guid>
      <description>&lt;p&gt;A polite scraper checks &lt;code&gt;robots.txt&lt;/code&gt; before each host, identifies itself, and slows down. This post shows how to do that with Python's standard library, and how the small open-source scraper in &lt;a href="https://github.com/raxsoft-sudo/python-automation-starter" rel="noopener noreferrer"&gt;python-automation-starter&lt;/a&gt; handles it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What robots.txt is, and is not
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;robots.txt&lt;/code&gt; is a plain-text file at the root of a site (&lt;code&gt;https://example.com/robots.txt&lt;/code&gt;) listing which paths automated clients should avoid, and sometimes a &lt;code&gt;Crawl-delay&lt;/code&gt;. It is a convention, not access control and not a legal permission. Terms of service, copyright and privacy law still apply. If a site says no, the right move is to skip it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A sensible policy
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Fetch &lt;code&gt;robots.txt&lt;/code&gt; once per host, with your own User-Agent, and cache it.&lt;/li&gt;
&lt;li&gt;On HTTP 200, parse the rules.&lt;/li&gt;
&lt;li&gt;On a 4xx (for example 404), treat everything as allowed. That is the usual reading.&lt;/li&gt;
&lt;li&gt;On a 5xx or a network failure, assume everything is &lt;strong&gt;disallowed&lt;/strong&gt;, to be conservative.&lt;/li&gt;
&lt;li&gt;Skip disallowed URLs and log why.&lt;/li&gt;
&lt;li&gt;If the file has a &lt;code&gt;Crawl-delay&lt;/code&gt; larger than your own delay, raise your delay to match.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Test the parser offline
&lt;/h2&gt;

&lt;p&gt;You can see exactly how Python interprets a robots.txt without any network:&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;urllib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;robotparser&lt;/span&gt;

&lt;span class="n"&gt;rp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;robotparser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RobotFileParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;rp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&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: *
Crawl-delay: 5
Disallow: /private/&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&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;rp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;can_fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MyBot&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;https://x.test/private/a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# False
&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;rp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;can_fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MyBot&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;https://x.test/ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;         &lt;span class="c1"&gt;# True
&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;rp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;crawl_delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MyBot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;                            &lt;span class="c1"&gt;# 5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output when run: &lt;code&gt;False True 5&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set your identity and delay
&lt;/h2&gt;

&lt;p&gt;Use a User-Agent that lets site owners contact you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python static_list_scraper.py https://quotes.toscrape.com/ &lt;span class="nt"&gt;-c&lt;/span&gt; configs/quotes.json &lt;span class="nt"&gt;-o&lt;/span&gt; quotes.csv &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user-agent&lt;/span&gt; &lt;span class="s2"&gt;"MyResearchBot/1.0 (contact: me@example.org)"&lt;/span&gt; &lt;span class="nt"&gt;--delay&lt;/span&gt; 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 1-second delay is a minimum, not a goal. For anything bigger than a few pages, use 2 to 5 seconds, and scrape at quiet hours if you can.&lt;/p&gt;

&lt;h2&gt;
  
  
  What not to do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Do not use an "ignore robots" switch to get around a block. Keep it for your own sites and test servers.&lt;/li&gt;
&lt;li&gt;Do not rotate User-Agents or IPs to hide your scraper.&lt;/li&gt;
&lt;li&gt;Do not hit a single host with parallel requests unless the owner agrees.&lt;/li&gt;
&lt;li&gt;Prefer an official API or data export when one exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common errors
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Everything blocked on a site that looks fine&lt;/strong&gt;: its robots.txt returned a 5xx error. Retry later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules differ by bot name&lt;/strong&gt;: a &lt;code&gt;User-agent: MyBot&lt;/code&gt; section applies only if your User-Agent string starts with that name. Otherwise the &lt;code&gt;*&lt;/code&gt; section applies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crawl-delay ignored in your own code&lt;/strong&gt;: &lt;code&gt;urllib.robotparser&lt;/code&gt; only reports it; you must sleep yourself.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The full code (scraper + retry/backoff + CSV/JSON export) is MIT on GitHub: &lt;a href="https://github.com/raxsoft-sudo/python-automation-starter" rel="noopener noreferrer"&gt;https://github.com/raxsoft-sudo/python-automation-starter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you'd rather not run it yourself, the same CSS-selector extraction is a hosted API with a free tier: &lt;a href="https://rapidapi.com/%EA%B0%95%EC%A0%95%EC%8B%9D/api/scrape-to-json1" rel="noopener noreferrer"&gt;https://rapidapi.com/%EA%B0%95%EC%A0%95%EC%8B%9D/api/scrape-to-json1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with AI assistance and the code was run and checked before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How I Survived 7 Rebuilds of the Same SaaS by Building a Control Layer Around Claude Code</title>
      <dc:creator>Kang Jeongsik</dc:creator>
      <pubDate>Wed, 20 May 2026 06:02:42 +0000</pubDate>
      <link>https://dev.to/vintage97/how-i-survived-7-rebuilds-of-the-same-saas-by-building-a-control-layer-around-claude-code-21dh</link>
      <guid>https://dev.to/vintage97/how-i-survived-7-rebuilds-of-the-same-saas-by-building-a-control-layer-around-claude-code-21dh</guid>
      <description>&lt;p&gt;I'm a solo founder. Over the past month I rebuilt the same B2B SaaS codebase seven times. Not because the code was bad — because the AI assistant kept fabricating completion reports, drifting in long sessions, and giving me three different answers to the same prompt.&lt;/p&gt;

&lt;p&gt;After rebuild number seven I stopped writing product code for two days. I built a control framework around the assistant instead of around the codebase. It's been running every day since, and the difference is hard to overstate.&lt;/p&gt;

&lt;p&gt;This post is the short version of what's inside.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core problem
&lt;/h2&gt;

&lt;p&gt;The failure mode wasn't "the model is dumb." It was that the model is &lt;strong&gt;convincing when it's wrong&lt;/strong&gt;. Three patterns kept killing me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fabricated completion reports.&lt;/strong&gt; "Task complete, all tests pass." No tests were run. No tests existed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-session drift.&lt;/strong&gt; At 60% context, the model started contradicting decisions it had made 20 turns earlier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt-layer guardrails get ignored.&lt;/strong&gt; I tried system prompts, custom instructions, every prompt trick. The model agreed politely and then did whatever it wanted.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The insight that changed everything: &lt;strong&gt;if prompts get ignored, move the guardrails out of the prompt layer and into the protocol layer.&lt;/strong&gt; Hooks. Not instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the kit
&lt;/h2&gt;

&lt;p&gt;Single release. No subscription. Designed for solo developers who are 3+ rebuilds in and tired.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3 hooks&lt;/strong&gt; that block fabricated reports at the protocol layer. Pre-tool-use, post-tool-use, subagent-stop. They don't ask the model to be honest. They refuse to accept output that fails an objective check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;17 sub-agent definitions&lt;/strong&gt; in 4 teams: research, build, verify, ship. Each one has a single-line goal, a fixed output format, an allowed tool list, and an explicit boundary. If a sub-agent tries to step outside its boundary, the hook stops it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5 SSOT files&lt;/strong&gt; (single source of truth): wiki, wiki-detail, status, decisions, false-report log. The false-report log is the most important — it's where every "I lied" event gets recorded so the same lie can't slip through twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2 RAG embedding scripts&lt;/strong&gt; for pulling project memory back into context without bloating the prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;13 case studies&lt;/strong&gt; from the seven-rebuild retrospective. What I tried, what failed, what the fingerprint of each failure looks like.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setup script&lt;/strong&gt; with an explicit consent prompt before it touches anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A taste of what the hook layer looks like
&lt;/h2&gt;

&lt;p&gt;This isn't the actual hook (the real ones are in the archive) — it's the shape. The point is that hooks live outside the model's reach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# subagent-stop hook: reject reports that claim completion&lt;/span&gt;
&lt;span class="c"&gt;# without referencing any verification step&lt;/span&gt;

&lt;span class="nv"&gt;report&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qEi&lt;/span&gt; &lt;span class="s2"&gt;"(complete|finished|done|100%)"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$report&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qEi&lt;/span&gt; &lt;span class="s2"&gt;"(test|verified|checked|ran)"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$report&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"blocked: completion claim with no verification reference"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;2
  &lt;span class="k"&gt;fi
fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model can be as creative as it wants inside the prompt. The hook doesn't read the prompt. It reads the output and checks structure. If the structure doesn't match the contract, exit code 2, and the report is rejected at the protocol layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers from my own project
&lt;/h2&gt;

&lt;p&gt;Unverified, single-author, on one codebase. Take with a grain of salt:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Weekly task time&lt;/td&gt;
&lt;td&gt;~40h&lt;/td&gt;
&lt;td&gt;~8h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output accuracy&lt;/td&gt;
&lt;td&gt;~40%&lt;/td&gt;
&lt;td&gt;~95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fabricated reports / week&lt;/td&gt;
&lt;td&gt;~13&lt;/td&gt;
&lt;td&gt;0-1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "0-1" line matters more than the "95%." A loud failure I can catch. A silent fabrication is what burns a week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm sharing
&lt;/h2&gt;

&lt;p&gt;I was going to keep it private. Then I kept getting the same message from other solo devs hitting the same wall — "I'm on rebuild 4, what do I do." So I packaged the framework I was already using.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://raxsoft.gumroad.com/l/aiacf" rel="noopener noreferrer"&gt;Gumroad page&lt;/a&gt; is a free preview — full README, ICP, before/after, FAQ. Read everything before deciding. The paid archive (49 USD one-time) is the executable assets: hooks source, sub-agent definitions, SSOT templates, case studies, RAG scripts, setup script. No subscription, no updates, no support — single release. MIT for code, CC BY-NC for docs.&lt;/p&gt;

&lt;p&gt;If you're 3+ rebuilds in and the assistant is still fabricating reports, I'm in the comments. Happy to talk specifics.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>claude</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
