feat(legal-sources): add OSHA machinery safety standards + international norms mapping
OSHA 29 CFR 1910 Subpart O (1910.211-1910.219) — complete machine guarding requirements. US federal law, public domain. International norms mapping table: China GB/T, Korea KS, India BIS equivalents to ISO/EN standards. Unfortunately all countries protect ISO copyright even for identical national adoptions (IDT). Only OSHA provides truly free machinery safety content. EU Excel harmonised standards list included for reference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
# TRBS + TRGS + ASR — Download-URLs
|
||||
|
||||
**Stand:** 2026-05-09
|
||||
**Quelle:** BAuA (Bundesanstalt für Arbeitsschutz und Arbeitsmedizin)
|
||||
**Lizenz:** Gemeinfrei (§5 UrhG — amtliche Bekanntmachungen)
|
||||
|
||||
## Anleitung
|
||||
|
||||
BAuA hat Bot-Schutz. Die PDFs müssen **manuell im Browser** heruntergeladen werden.
|
||||
Jede URL führt zur BAuA-Detailseite → dort den PDF-Download-Link klicken.
|
||||
|
||||
Alle heruntergeladenen PDFs in dieses Verzeichnis legen:
|
||||
```
|
||||
legal-sources/trbs-trgs-asr/
|
||||
```
|
||||
|
||||
Dateinamen-Konvention: `trbs_1111.pdf`, `trgs_400.pdf`, `asr_a1_3.pdf`
|
||||
|
||||
---
|
||||
|
||||
## TRBS — Technische Regeln für Betriebssicherheit (~35 Dokumente)
|
||||
|
||||
### 1000er Reihe (Allgemein)
|
||||
1. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1001.html — TRBS 1001: Struktur und Anwendung
|
||||
2. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1111.html — TRBS 1111: Gefährdungsbeurteilung
|
||||
3. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1112.html — TRBS 1112: Instandhaltung
|
||||
4. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1112-Teil-1.html — TRBS 1112 Teil 1: Explosionsgefährdungen bei Instandhaltung
|
||||
5. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1115.html — TRBS 1115: Sicherheitsrelevante MSR-Einrichtungen
|
||||
6. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1115-Teil-1.html — TRBS 1115 Teil 1: Cybersicherheit für MSR
|
||||
7. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1116.html — TRBS 1116: Qualifikation und Unterweisung
|
||||
8. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1121.html — TRBS 1121: Änderungen an Aufzugsanlagen
|
||||
9. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1122.html — TRBS 1122: Änderungen an Anlagen (§1 Abs.2 Nr.4)
|
||||
10. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1123.html — TRBS 1123: Änderungen an Anlagen (§1 Abs.2 Nr.3)
|
||||
11. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1151.html — TRBS 1151: Mensch-Arbeitsmittel-Schnittstelle, Ergonomie
|
||||
12. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1201.html — TRBS 1201: Prüfungen von Arbeitsmitteln
|
||||
13. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1201-Teil-1.html — TRBS 1201 Teil 1: Prüfung in Ex-Bereichen
|
||||
14. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1201-Teil-2.html — TRBS 1201 Teil 2: Prüfung bei Dampf/Druck
|
||||
15. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1201-Teil-4.html — TRBS 1201 Teil 4: Prüfung von Aufzugsanlagen
|
||||
16. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1201-Teil-5.html — TRBS 1201 Teil 5: Prüfung Lager-/Tankstellen
|
||||
17. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-1203.html — TRBS 1203: Befähigte Personen
|
||||
|
||||
### 2000er Reihe (Gefährdungsbezogen)
|
||||
18. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-2111.html — TRBS 2111: Mechanische Gefährdungen
|
||||
19. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-2111-Teil-1.html — TRBS 2111 Teil 1: Kontrolliert bewegte Teile
|
||||
20. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-2111-Teil-2.html — TRBS 2111 Teil 2: Unkontrolliert bewegte Teile
|
||||
21. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-2111-Teil-3.html — TRBS 2111 Teil 3: Gefährliche Oberflächen
|
||||
22. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-2111-Teil-4.html — TRBS 2111 Teil 4: Mobile Arbeitsmittel
|
||||
23. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-2121.html — TRBS 2121: Absturzgefährdung
|
||||
24. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-2141.html — TRBS 2141: Dampf und Druck
|
||||
25. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-2141-Teil-1.html — TRBS 2141 Teil 1: Versagen drucktragender Wandung
|
||||
26. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-2152.html — TRBS 2152: Explosionsfähige Atmosphäre
|
||||
27. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-2152-Teil-1.html — TRBS 2152 Teil 1: Beurteilung Explosionsgefährdung
|
||||
28. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-2152-Teil-2.html — TRBS 2152 Teil 2: Vermeidung Ex-Atmosphäre
|
||||
29. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-2152-Teil-3.html — TRBS 2152 Teil 3: Vermeidung Entzündung
|
||||
30. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-2152-Teil-4.html — TRBS 2152 Teil 4: Konstruktiver Explosionsschutz
|
||||
31. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-2181.html — TRBS 2181: Eingeschlossensein in Personenaufnahmemitteln
|
||||
32. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-2210.html — TRBS 2210: Wechselwirkungen
|
||||
|
||||
### 3000er Reihe (Spezifisch)
|
||||
33. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-3121.html — TRBS 3121: Betrieb von Aufzugsanlagen
|
||||
34. https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS-3151.html — TRBS 3151: Brand-/Explosionsschutz Tankstellen
|
||||
|
||||
---
|
||||
|
||||
## TRGS — Technische Regeln für Gefahrstoffe (~50 Dokumente)
|
||||
|
||||
### 200er Reihe (Einstufung/Kennzeichnung)
|
||||
35. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-200.html — TRGS 200: Einstufung und Kennzeichnung
|
||||
36. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-201.html — TRGS 201: Einstufung und Kennzeichnung bei Tätigkeiten
|
||||
37. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-220.html — TRGS 220: Sicherheitsdatenblatt
|
||||
|
||||
### 400er Reihe (Gefährdungsbeurteilung)
|
||||
38. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-400.html — TRGS 400: Gefährdungsbeurteilung Gefahrstoffe
|
||||
39. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-401.html — TRGS 401: Hautgefährdung
|
||||
40. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-402.html — TRGS 402: Inhalative Exposition
|
||||
41. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-406.html — TRGS 406: Sensibilisierende Stoffe
|
||||
42. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-407.html — TRGS 407: Tätigkeiten mit Gasen
|
||||
43. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-410.html — TRGS 410: Expositionsverzeichnis krebserzeugende Stoffe
|
||||
44. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-420.html — TRGS 420: Verfahrens- und stoffspezifische Kriterien
|
||||
45. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-430.html — TRGS 430: Isocyanate
|
||||
46. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-460.html — TRGS 460: Stand der Technik
|
||||
|
||||
### 500er Reihe (Schutzmaßnahmen)
|
||||
47. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-500.html — TRGS 500: Schutzmaßnahmen
|
||||
48. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-504.html — TRGS 504: Tätigkeiten mit Blei
|
||||
49. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-505.html — TRGS 505: Oberflächenbehandlung in Räumen
|
||||
50. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-507.html — TRGS 507: Oberflächenbehandlung in Räumen und Behältern
|
||||
51. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-509.html — TRGS 509: Lagern von flüssigen/festen Gefahrstoffen in ortsfesten Behältern
|
||||
52. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-510.html — TRGS 510: Lagerung von Gefahrstoffen in ortsbeweglichen Behältern
|
||||
53. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-512.html — TRGS 512: Begasungen
|
||||
54. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-513.html — TRGS 513: Tätigkeiten an Sterilisatoren mit ETO
|
||||
55. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-519.html — TRGS 519: Asbest
|
||||
56. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-520.html — TRGS 520: Errichtung und Betrieb von Sammelstellen
|
||||
57. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-521.html — TRGS 521: Abbruch/Sanierung alte Mineralwolle
|
||||
58. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-522.html — TRGS 522: Raumdesinfektion mit Formaldehyd
|
||||
59. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-523.html — TRGS 523: Schädlingsbekämpfung mit sehr giftigen/giftigen Stoffen
|
||||
60. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-524.html — TRGS 524: Schutzmaßnahmen bei kontaminierten Bereichen
|
||||
61. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-525.html — TRGS 525: Gefahrstoffe in Einrichtungen der medizinischen Versorgung
|
||||
62. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-526.html — TRGS 526: Laboratorien
|
||||
63. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-527.html — TRGS 527: Tätigkeiten mit Nanomaterialien
|
||||
64. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-528.html — TRGS 528: Schweißtechnische Arbeiten
|
||||
65. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-529.html — TRGS 529: Tätigkeiten bei Biogasanlagen
|
||||
66. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-530.html — TRGS 530: Friseurhandwerk
|
||||
67. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-551.html — TRGS 551: Teer und andere PAK-haltige Stoffe
|
||||
68. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-552.html — TRGS 552: N-Nitrosamine
|
||||
69. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-553.html — TRGS 553: Holzstaub
|
||||
70. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-554.html — TRGS 554: Abgase von Dieselmotoren
|
||||
71. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-555.html — TRGS 555: Betriebsanweisung und Information
|
||||
72. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-557.html — TRGS 557: Dioxine
|
||||
73. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-558.html — TRGS 558: Quarzfeinstaub
|
||||
74. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-559.html — TRGS 559: Mineralischer Staub
|
||||
75. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-561.html — TRGS 561: Krebserzeugende Metalle
|
||||
|
||||
### 600er Reihe (Substitution)
|
||||
76. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-600.html — TRGS 600: Substitution
|
||||
77. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-610.html — TRGS 610: Ersatzstoffe und Ersatzverfahren für chrysotilhaltigen Asbest
|
||||
78. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-617.html — TRGS 617: Ersatzstoffe für Kühlschmierstoffe
|
||||
79. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-619.html — TRGS 619: Substitution für chromat-haltige Beschichtungsstoffe
|
||||
|
||||
### 700er Reihe (Brand-/Explosionsschutz)
|
||||
80. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-720.html — TRGS 720: Gefährliche explosionsfähige Gemische
|
||||
81. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-721.html — TRGS 721: Beurteilung Explosionsgefährdung
|
||||
82. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-722.html — TRGS 722: Vermeidung explosionsfähiger Gemische
|
||||
83. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-723.html — TRGS 723: Gefährliche explosionsfähige Gemische – Vermeidung Entzündung
|
||||
84. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-724.html — TRGS 724: Gefährliche explosionsfähige Gemische – Konstruktiver Schutz
|
||||
85. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-725.html — TRGS 725: Gefährliche explosionsfähige Gemische – MSR-Einrichtungen
|
||||
86. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-726.html — TRGS 726: Sauerstoffgrenzkonzentration
|
||||
87. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-727.html — TRGS 727: Vermeidung von Zündgefahren (elektrostatisch)
|
||||
88. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-741.html — TRGS 741: Organische Peroxide
|
||||
89. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-745.html — TRGS 745: Ortsbewegliche Druckgasbehälter
|
||||
90. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-746.html — TRGS 746: Ortsfeste Druckanlagen für Gase
|
||||
91. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-751.html — TRGS 751: Vermeidung von Brand-/Explosionsgefahren Tankstellen
|
||||
92. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-800.html — TRGS 800: Brandschutzmaßnahmen
|
||||
|
||||
### 900er Reihe (Grenzwerte)
|
||||
93. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-900.html — TRGS 900: Arbeitsplatzgrenzwerte
|
||||
94. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-903.html — TRGS 903: Biologische Grenzwerte
|
||||
95. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-905.html — TRGS 905: Verzeichnis krebserzeugender Stoffe
|
||||
96. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-906.html — TRGS 906: Verzeichnis krebserzeugender Verfahren
|
||||
97. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-907.html — TRGS 907: Verzeichnis sensibilisierender Stoffe
|
||||
98. https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS-910.html — TRGS 910: Risikobezogenes Maßnahmenkonzept krebserzeugende Stoffe
|
||||
|
||||
---
|
||||
|
||||
## ASR — Arbeitsstättenregeln (~21 Dokumente)
|
||||
|
||||
99. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-V3.html — ASR V3: Gefährdungsbeurteilung
|
||||
100. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-V3a-2.html — ASR V3a.2: Barrierefreie Gestaltung
|
||||
101. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A1-2.html — ASR A1.2: Raumabmessungen und Bewegungsflächen
|
||||
102. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A1-3.html — ASR A1.3: Sicherheits-/Gesundheitsschutzkennzeichnung
|
||||
103. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A1-5-1-2.html — ASR A1.5/1,2: Fußböden
|
||||
104. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A1-6.html — ASR A1.6: Fenster, Oberlichter
|
||||
105. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A1-7.html — ASR A1.7: Türen und Tore
|
||||
106. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A1-8.html — ASR A1.8: Verkehrswege
|
||||
107. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A2-1.html — ASR A2.1: Schutz vor Absturz
|
||||
108. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A2-2.html — ASR A2.2: Maßnahmen gegen Brände
|
||||
109. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A2-3.html — ASR A2.3: Fluchtwege und Notausgänge
|
||||
110. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A3-4.html — ASR A3.4: Beleuchtung und Sichtverbindung
|
||||
111. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A3-4-3.html — ASR A3.4/3: Sicherheitsbeleuchtung
|
||||
112. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A3-5.html — ASR A3.5: Raumtemperatur
|
||||
113. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A3-6.html — ASR A3.6: Lüftung
|
||||
114. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A3-7.html — ASR A3.7: Lärm
|
||||
115. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A4-1.html — ASR A4.1: Sanitärräume
|
||||
116. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A4-2.html — ASR A4.2: Pausen-/Bereitschaftsräume
|
||||
117. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A4-3.html — ASR A4.3: Erste-Hilfe-Räume
|
||||
118. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A4-4.html — ASR A4.4: Unterkünfte
|
||||
119. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A5-2.html — ASR A5.2: Baustellen
|
||||
120. https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR-A6.html — ASR A6: Bildschirmarbeit
|
||||
|
||||
---
|
||||
|
||||
**Gesamt: 120 Dokumente** (34 TRBS + 64 TRGS + 22 ASR)
|
||||
|
||||
**Hinweis:** Einige URLs könnten leicht abweichen (Bindestriche vs. Punkte). Im Browser die BAuA-Übersichtsseite nutzen und von dort die PDFs einzeln herunterladen:
|
||||
- https://www.baua.de/DE/Angebote/Regelwerk/TRBS/TRBS.html
|
||||
- https://www.baua.de/DE/Angebote/Regelwerk/TRGS/TRGS.html
|
||||
- https://www.baua.de/DE/Angebote/Regelwerk/ASR/ASR.html
|
||||
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
BAuA Regulatory Crawler — TRBS, TRGS, ASR
|
||||
|
||||
Crawls the BAuA website using Playwright (headless browser),
|
||||
extracts PDF links, downloads all documents.
|
||||
|
||||
Usage:
|
||||
python3 crawl_baua.py # download all
|
||||
python3 crawl_baua.py --category trbs # only TRBS
|
||||
python3 crawl_baua.py --dry-run # list PDFs without downloading
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s"
|
||||
)
|
||||
logger = logging.getLogger("baua-crawler")
|
||||
|
||||
BASE_URL = "https://www.baua.de"
|
||||
OUTPUT_DIR = Path(__file__).parent / "pdfs"
|
||||
REGISTRY_FILE = Path(__file__).parent / "source_registry.json"
|
||||
|
||||
CATEGORIES = {
|
||||
"trbs": {
|
||||
"url": f"{BASE_URL}/DE/Angebote/Regelwerk/TRBS/TRBS.html",
|
||||
"name": "Technische Regeln für Betriebssicherheit",
|
||||
"source_type": "technical_rule",
|
||||
"legal_basis": "BetrSichV",
|
||||
},
|
||||
"trgs": {
|
||||
"url": f"{BASE_URL}/DE/Angebote/Regelwerk/TRGS/TRGS.html",
|
||||
"name": "Technische Regeln für Gefahrstoffe",
|
||||
"source_type": "technical_rule",
|
||||
"legal_basis": "GefStoffV",
|
||||
},
|
||||
"asr": {
|
||||
"url": f"{BASE_URL}/DE/Angebote/Regelwerk/ASR/ASR.html",
|
||||
"name": "Arbeitsstättenregeln",
|
||||
"source_type": "technical_rule",
|
||||
"legal_basis": "ArbStättV",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def crawl_index(page, category: str, config: dict) -> list[dict]:
|
||||
"""Crawl index page and extract detail page links."""
|
||||
logger.info("Crawling %s index: %s", category.upper(), config["url"])
|
||||
page.goto(config["url"], wait_until="networkidle", timeout=30000)
|
||||
time.sleep(3) # Wait for BunnyShield
|
||||
|
||||
# Extract all links to detail pages
|
||||
links = page.query_selector_all("a[href]")
|
||||
detail_urls = []
|
||||
seen = set()
|
||||
|
||||
for link in links:
|
||||
href = link.get_attribute("href") or ""
|
||||
text = (link.inner_text() or "").strip()
|
||||
|
||||
# Match pattern: /DE/Angebote/Regelwerk/TRBS/TRBS-1111 (no .html!)
|
||||
# ASR uses ASR-A1-3 (not ASR-ASR-A1-3)
|
||||
base_pattern = f"/DE/Angebote/Regelwerk/{category.upper()}/"
|
||||
is_detail = (base_pattern in href
|
||||
and "#" not in href and "?" not in href
|
||||
and href != base_pattern.rstrip("/")
|
||||
and href.split("/")[-1] != category.upper())
|
||||
if is_detail and href not in seen:
|
||||
full_url = urljoin(BASE_URL, href)
|
||||
seen.add(href)
|
||||
|
||||
# Extract regulation number from URL
|
||||
filename = href.split("/")[-1]
|
||||
detail_urls.append({
|
||||
"detail_url": full_url,
|
||||
"title": text[:200] if text else filename,
|
||||
"filename": filename,
|
||||
"category": category,
|
||||
})
|
||||
|
||||
logger.info("Found %d detail pages for %s", len(detail_urls), category.upper())
|
||||
return detail_urls
|
||||
|
||||
|
||||
def extract_pdf_url(page, detail: dict) -> dict:
|
||||
"""Visit detail page and extract PDF download link."""
|
||||
try:
|
||||
page.goto(detail["detail_url"], wait_until="networkidle", timeout=30000)
|
||||
time.sleep(2)
|
||||
|
||||
# Strategy 1: Direct PDF link
|
||||
pdf_links = page.query_selector_all('a[href$=".pdf"]')
|
||||
for link in pdf_links:
|
||||
href = link.get_attribute("href") or ""
|
||||
if href:
|
||||
detail["pdf_url"] = urljoin(BASE_URL, href)
|
||||
return detail
|
||||
|
||||
# Strategy 2: Download button with data attribute
|
||||
download_btns = page.query_selector_all("[data-download-url]")
|
||||
for btn in download_btns:
|
||||
url = btn.get_attribute("data-download-url") or ""
|
||||
if url and ".pdf" in url:
|
||||
detail["pdf_url"] = urljoin(BASE_URL, url)
|
||||
return detail
|
||||
|
||||
# Strategy 3: Links containing "pdf" or "download"
|
||||
all_links = page.query_selector_all("a[href]")
|
||||
for link in all_links:
|
||||
href = link.get_attribute("href") or ""
|
||||
text = (link.inner_text() or "").lower()
|
||||
if (".pdf" in href or "download" in text) and href:
|
||||
detail["pdf_url"] = urljoin(BASE_URL, href)
|
||||
return detail
|
||||
|
||||
# Strategy 4: Check for blob/dynamic download
|
||||
download_links = page.query_selector_all(
|
||||
'a[href*="blob"], a[href*="download"], a[href*="__blob"]'
|
||||
)
|
||||
for link in download_links:
|
||||
href = link.get_attribute("href") or ""
|
||||
if href:
|
||||
detail["pdf_url"] = urljoin(BASE_URL, href)
|
||||
return detail
|
||||
|
||||
logger.warning("No PDF found for %s", detail["filename"])
|
||||
detail["pdf_url"] = None
|
||||
return detail
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error on %s: %s", detail["detail_url"], e)
|
||||
detail["pdf_url"] = None
|
||||
return detail
|
||||
|
||||
|
||||
def download_pdf(page, detail: dict, output_dir: Path) -> dict:
|
||||
"""Download PDF and compute hash."""
|
||||
if not detail.get("pdf_url"):
|
||||
return detail
|
||||
|
||||
cat = detail["category"]
|
||||
safe_name = re.sub(r"[^a-zA-Z0-9_\-]", "_", detail["filename"]).lower()
|
||||
pdf_path = output_dir / cat / f"{safe_name}.pdf"
|
||||
pdf_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if pdf_path.exists():
|
||||
logger.info(" Already exists: %s", pdf_path.name)
|
||||
detail["local_path"] = str(pdf_path)
|
||||
detail["sha256"] = hashlib.sha256(pdf_path.read_bytes()).hexdigest()
|
||||
return detail
|
||||
|
||||
try:
|
||||
with page.expect_download(timeout=60000) as download_info:
|
||||
page.goto(detail["pdf_url"], timeout=30000)
|
||||
download = download_info.value
|
||||
download.save_as(str(pdf_path))
|
||||
except Exception:
|
||||
# Fallback: direct download via response
|
||||
try:
|
||||
response = page.request.get(detail["pdf_url"])
|
||||
if response.ok:
|
||||
pdf_path.write_bytes(response.body())
|
||||
else:
|
||||
logger.error(" Download failed: %s (HTTP %d)",
|
||||
detail["filename"], response.status)
|
||||
return detail
|
||||
except Exception as e:
|
||||
logger.error(" Download failed: %s — %s", detail["filename"], e)
|
||||
return detail
|
||||
|
||||
size = pdf_path.stat().st_size
|
||||
detail["local_path"] = str(pdf_path)
|
||||
detail["sha256"] = hashlib.sha256(pdf_path.read_bytes()).hexdigest()
|
||||
detail["size_bytes"] = size
|
||||
logger.info(" Downloaded: %s (%.1f KB)", pdf_path.name, size / 1024)
|
||||
return detail
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--category", choices=["trbs", "trgs", "asr"],
|
||||
help="Only crawl one category")
|
||||
parser.add_argument("--dry-run", action="store_true",
|
||||
help="List PDFs without downloading")
|
||||
parser.add_argument("--headless", action="store_true", default=True)
|
||||
parser.add_argument("--no-headless", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
headless = not args.no_headless
|
||||
categories = [args.category] if args.category else list(CATEGORIES.keys())
|
||||
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
registry = []
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=headless)
|
||||
context = browser.new_context(
|
||||
user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/120.0.0.0 Safari/537.36"
|
||||
)
|
||||
page = context.new_page()
|
||||
|
||||
for cat in categories:
|
||||
config = CATEGORIES[cat]
|
||||
logger.info("\n=== %s ===", cat.upper())
|
||||
|
||||
# Step 1: Crawl index
|
||||
details = crawl_index(page, cat, config)
|
||||
|
||||
# Step 2: Extract PDF URLs
|
||||
for i, detail in enumerate(details):
|
||||
logger.info("[%d/%d] %s", i + 1, len(details), detail["filename"])
|
||||
extract_pdf_url(page, detail)
|
||||
time.sleep(1) # Be polite
|
||||
|
||||
# Step 3: Download PDFs
|
||||
if not args.dry_run:
|
||||
for detail in details:
|
||||
download_pdf(page, detail, OUTPUT_DIR)
|
||||
time.sleep(0.5)
|
||||
|
||||
# Add metadata
|
||||
for detail in details:
|
||||
detail["source_type"] = config["source_type"]
|
||||
detail["legal_basis"] = config["legal_basis"]
|
||||
detail["license_rule"] = 1 # §5 UrhG, gemeinfrei
|
||||
detail["jurisdiction"] = "DE"
|
||||
|
||||
registry.extend(details)
|
||||
|
||||
browser.close()
|
||||
|
||||
# Save registry
|
||||
REGISTRY_FILE.write_text(json.dumps(registry, indent=2, ensure_ascii=False))
|
||||
logger.info("\nRegistry saved: %s (%d entries)", REGISTRY_FILE, len(registry))
|
||||
|
||||
# Summary
|
||||
total = len(registry)
|
||||
with_pdf = sum(1 for r in registry if r.get("pdf_url"))
|
||||
downloaded = sum(1 for r in registry if r.get("local_path"))
|
||||
logger.info("Total: %d | PDF found: %d | Downloaded: %d", total, with_pdf, downloaded)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Ingest downloaded TRBS/TRGS/ASR PDFs into Qdrant via RAG Service.
|
||||
|
||||
Reads the source_registry.json and uploads each PDF to the RAG service.
|
||||
|
||||
Usage:
|
||||
python3 ingest_to_qdrant.py # ingest all
|
||||
python3 ingest_to_qdrant.py --category trbs # only TRBS
|
||||
python3 ingest_to_qdrant.py --dry-run # list without uploading
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s"
|
||||
)
|
||||
logger = logging.getLogger("ingest-trbs")
|
||||
|
||||
REGISTRY_FILE = Path(__file__).parent / "source_registry.json"
|
||||
RAG_URL = "https://macmini:8097/api/v1/documents/upload"
|
||||
COLLECTION = "bp_compliance_ce" # Same collection as other CE documents
|
||||
|
||||
|
||||
def ingest_pdf(entry: dict) -> dict:
|
||||
"""Upload a single PDF to the RAG service."""
|
||||
local_path = entry.get("local_path", "")
|
||||
if not local_path or not Path(local_path).exists():
|
||||
return {"status": "skipped", "reason": "no local file"}
|
||||
|
||||
pdf_path = Path(local_path)
|
||||
category = entry.get("category", "unknown")
|
||||
filename = entry.get("filename", pdf_path.name)
|
||||
title = entry.get("title", filename)
|
||||
|
||||
metadata = {
|
||||
"source": title,
|
||||
"regulation_id": f"{category}_{filename}".lower().replace("-", "_"),
|
||||
"jurisdiction": "DE",
|
||||
"source_type": "technical_rule",
|
||||
"license_rule": 1,
|
||||
"category": category,
|
||||
"legal_basis": entry.get("legal_basis", ""),
|
||||
}
|
||||
|
||||
try:
|
||||
with open(pdf_path, "rb") as f:
|
||||
files = {"file": (pdf_path.name, f, "application/pdf")}
|
||||
data = {
|
||||
"collection": COLLECTION,
|
||||
"data_type": "legal",
|
||||
"use_case": "compliance",
|
||||
"year": "2026",
|
||||
"chunk_size": "512",
|
||||
"chunk_overlap": "50",
|
||||
"metadata_json": json.dumps(metadata),
|
||||
}
|
||||
resp = httpx.post(RAG_URL, files=files, data=data, timeout=300.0, verify=False)
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
return {
|
||||
"status": "ok",
|
||||
"document_id": result.get("document_id", ""),
|
||||
"chunks": result.get("chunks_count", 0),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"status": "error", "reason": str(e)}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--category", choices=["trbs", "trgs", "asr"])
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
registry = json.loads(REGISTRY_FILE.read_text())
|
||||
if args.category:
|
||||
registry = [e for e in registry if e.get("category") == args.category]
|
||||
|
||||
logger.info("Ingesting %d documents into Qdrant (%s)", len(registry), COLLECTION)
|
||||
|
||||
total_ok = 0
|
||||
total_chunks = 0
|
||||
total_err = 0
|
||||
|
||||
for i, entry in enumerate(registry):
|
||||
if not entry.get("local_path"):
|
||||
continue
|
||||
|
||||
if args.dry_run:
|
||||
logger.info("[%d/%d] %s — %s (dry-run)",
|
||||
i + 1, len(registry), entry["filename"], entry.get("title", "")[:60])
|
||||
continue
|
||||
|
||||
logger.info("[%d/%d] %s", i + 1, len(registry), entry["filename"])
|
||||
result = ingest_pdf(entry)
|
||||
|
||||
if result["status"] == "ok":
|
||||
total_ok += 1
|
||||
total_chunks += result["chunks"]
|
||||
logger.info(" → %d chunks indexed", result["chunks"])
|
||||
else:
|
||||
total_err += 1
|
||||
logger.error(" → %s: %s", result["status"], result.get("reason", ""))
|
||||
|
||||
time.sleep(1) # Be gentle
|
||||
|
||||
logger.info("\nDone: %d OK (%d chunks), %d errors, %d total",
|
||||
total_ok, total_chunks, total_err, len(registry))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user