Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8"/>
- <meta content="IE=edge" http-equiv="X-UA-Compatible"/>
- <title>
- sacrificing accessibility for not getting web-scraped
- </title>
- <meta content="width=device-width, initial-scale=1" name="viewport"/>
- <style>
- @font-face {
- font-family: "Space Grotesk";
- src: url("../fonts/SpaceGrotesk-VariableFont.ttf") format("truetype");
- font-weight: 100 900;
- }
- @font-face {
- font-family: "Mulish";
- src: url("../fonts/Mulish-VariableFont.ttf") format("truetype");
- font-weight: 100 900;
- }
- /* Fill whole page and keep footer at bottom. Reset margins. */
- html,
- body {
- height: 100%;
- margin: 0;
- }
- /* Keep scrollbar always visible to keep spacing consistent. */
- html {
- overflow-y: scroll;
- }
- body {
- display: flex;
- flex-direction: column;
- background-color: rgb(255, 230, 187);
- font-family: "Mulish", sans-serif;
- font-optical-sizing: auto;
- font-weight: 400;
- font-style: normal;
- font-size: 18px;
- }
- h1,
- h2 {
- text-align: center;
- font-family: "Mulish", sans-serif;
- font-optical-sizing: auto;
- font-weight: 600;
- font-style: normal;
- }
- p {
- margin-top: 0.5em;
- }
- /* Override justification inside tables */
- table,
- th,
- td,
- .footnotes {
- text-align: left;
- }
- header h1 {
- padding: 0.25em 0em;
- margin: 0em;
- font-weight: 700;
- font-style: bold;
- font-size: 70px;
- font-family: "Space Grotesk", sans-serif;
- font-optical-sizing: auto;
- font-style: normal;
- font-weight: 600;
- }
- a {
- text-decoration: none;
- font-weight: 700;
- transition: color 0.66s ease-in-out;
- color: #c07139;
- }
- a:hover {
- color: #8b4513;
- }
- /* Set consistent width and margins. Justify text. */
- header,
- main,
- footer {
- width: 600px;
- margin: 0 auto;
- text-align: justify;
- }
- /* spacer-div keeps footer at the bottom of the page. */
- .spacer {
- flex: 1;
- }
- footer {
- padding: 1.5em;
- text-align: center;
- }
- @font-face {
- font-family: "Mulish-scrambled";
- src: url("../fonts/Mulish-Regular-scrambled.ttf") format("truetype");
- font-weight: 100 900;
- }
- main {
- font-family: "Mulish-scrambled", sans-serif;
- font-optical-sizing: auto;
- font-weight: 400;
- font-style: normal;
- font-size: 18px;
- }
- .code-snippet {
- background: rgb(192, 113, 57);
- overflow-x: auto;
- padding: 0.5em;
- margin: 0.5em;
- }
- details {
- padding: 0.5em 0;
- }
- </style>
- </head>
- <body>
- <header>
- <h1>
- <a href="/index.html">TIL SCHÜNEMANN</a>
- </h1> </header>
- <main>
- <div class="content">
- <h1>sacrificing accessibility for not getting web-scraped</h1>
- <p>
- LLMs have taken the world by a storm, and need ever-increasing training data to improve.
- Copyright laws get broken, content gets aggressively scraped, and even though you might have deleted your original work, it might just show up because it got cached or archived at some point.
- </p>
- <p>
- Now, if you subscribe to the idea that your content shouldn't be used for training, you don't have much say.
- I wondered how I personally would mitigate this on a technical level.
- </p>
- <h2>et tu, caesar?</h2>
- <p>
- In my linear algebra class we discussed <a href="#footnote-1" id="ref-1">the caesar cipher<sup>[1]</sup></a> as a simple encryption algorithm:
- Every character gets shifted by n characters. If you know (or guess) the shift, you can figure out the original text.
- Brute force or character heuristics break this easily.
- </p>
- <p>
- But we can apply this substitution more generally to a font!
- A font contains a cmap (character map), which maps codepoints and glyphs. A codepoint defines the character, or complex symbol, and the glyph represents the visual shape.
- We scramble the font´s codepoint-glyph-mapping, and adjust the text with the inverse of the scramble, so it stays intact for our readers.
- It displays correctly, but the inspected (or scraped) HTML stays scrambled. Theoretically, you could apply a different scramble to each request.
- </p>
- <p>
- This works as long as scrapers don't use OCR for handling edge cases like this, but I don't think it would be feasible.
- </p>
- <p>
- I also tested if ChatGPT could decode a ciphertext if I'd tell it that a substitution cipher was used, and after some back and forth, it gave me the result: <i>One day Alice went down a rabbit hole, and found herself in Wonderland, a strange and magical place filled with...</i>
- </p>
- <p>
- ...which funnily didn't resemble the original text at all! This might have happened due to the training corpus containing <a href="#footnote-2" id="ref-2">Alice and Bob<sup>[2]</sup></a> as standard party labels for showcasing encryption.
- </p>
- <p>
- <details>
- <summary>The code I used for testing: (click to expand)</summary>
- <div class="code-snippet">
- <code style="white-space: pre;"># /// script
- # requires-python = ">=3.12"
- # dependencies = [
- # "bs4",
- # "fonttools",
- # ]
- # ///
- import random
- import string
- from typing import Dict
- from bs4 import BeautifulSoup
- from fontTools.ttLib import TTFont
- def scramble_font(seed: int = 1234) -> Dict[str, str]:
- random.seed(seed)
- font = TTFont("src/fonts/Mulish-Regular.ttf")
- # Pick a Unicode cmap (Windows BMP preferred)
- cmap_table = None
- for table in font["cmap"].tables:
- if table.isUnicode() and table.platformID == 3:
- break
- cmap_table = table
- cmap = cmap_table.cmap
- # Filter codepoints for a-z and A-Z
- codepoints = [cp for cp in cmap.keys() if chr(cp) in string.ascii_letters]
- glyphs = [cmap[cp] for cp in codepoints]
- shuffled_glyphs = glyphs[:]
- random.shuffle(shuffled_glyphs)
- # Create new mapping
- scrambled_cmap = dict(zip(codepoints, shuffled_glyphs, strict=True))
- cmap_table.cmap = scrambled_cmap
- translation_mapping = {}
- for original_cp, original_glyph in zip(codepoints, glyphs, strict=True):
- for new_cp, new_glyph in scrambled_cmap.items():
- if new_glyph == original_glyph:
- translation_mapping[chr(original_cp)] = chr(new_cp)
- break
- font.save("src/fonts/Mulish-Regular-scrambled.ttf")
- return translation_mapping
- def scramble_html(
- input: str,
- translation_mapping: Dict[str, str],
- ) -> str:
- def apply_cipher(text):
- repl = "".join(translation_mapping.get(c, c) for c in text)
- return repl
- # Read HTML file
- soup = BeautifulSoup(input, "html.parser")
- # Find all main elements
- main_elements = soup.find_all("main")
- skip_tags = {"code", "h1", "h2"}
- # Apply cipher only to text within main
- for main in main_elements:
- for elem in main.find_all(string=True):
- if elem.parent.name not in skip_tags:
- elem.replace_with(apply_cipher(elem))
- return str(soup)
- </code>
- </div>
- </details>
- </p>
- <h2>drawbacks</h2>
- <p>
- There is no free lunch, and this method comes with major drawbacks:
- <ul>
- <li>copy-paste gets broken</li>
- <li>accessibility for screen readers or non-graphical browsers like w3m is gone</li>
- <li>your search rank will drop</li>
- <li>font-kerning could get messed up (if you are not using a monospace font)</li>
- <li>probably more</li>
- </ul>
- On the plus side, you read this article using my own scrambled font. Take this, web scrapers!
- </p>
- </div>
- <div class="footnotes">
- <h2>footnotes</h2>
- <p>
- You can click on the footnote index to jump back:
- <ul>
- <li id="footnote-1">
- <a href="#ref-1"><sup>[1]</sup></a> <a href="https://en.wikipedia.org/wiki/Caesar_cipher">https://en.wikipedia.org/wiki/Caesar_cipher</a>
- </li>
- <li id="footnote-2">
- <a href="#ref-2"><sup>[2]</sup></a> <a href="https://en.wikipedia.org/wiki/Alice_and_Bob">https://en.wikipedia.org/wiki/Alice_and_Bob</a>
- </li>
- </ul>
- </p>
- </div>
- </main>
- <div class="spacer"></div>
- <footer>
- made with <span style="color: saddlebrown">♥</span> by Til Schünemann </footer>
- <script defer src="https://static.cloudflareinsights.com/beacon.min.js/vcd15cbe7772f49c399c6a5babf22c1241717689176015" integrity="sha512-ZpsOmlRQV6y907TI0dKBHq9Md29nnaEIPlkf84rnaERnq6zvWvPUqr2ft8M1aS28oN72PdrCzSjY4U6VaAw1EQ==" data-cf-beacon='{"version":"2024.11.0","token":"f6d3e9f932164d77b025bcfdfbeae066","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment