datamatters24 commited on
Commit
aa8f7e4
·
verified ·
1 Parent(s): 63eed31

Upload notebooks/04_forensic/43_metadata_extraction.ipynb with huggingface_hub

Browse files
notebooks/04_forensic/43_metadata_extraction.ipynb ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# 43 - Metadata Extraction\n",
8
+ "\n",
9
+ "Pipeline notebook that extracts PDF metadata and scans for classification stamps.\n",
10
+ "\n",
11
+ "**Document features stored:**\n",
12
+ "- `pdf_metadata` -- JSONB with author, title, subject, creator, producer, dates\n",
13
+ "- `classification_stamps` -- JSONB list of stamps found (CONFIDENTIAL, CLASSIFIED, etc.)\n",
14
+ "\n",
15
+ "Incremental: skips documents that already have `pdf_metadata` in `document_features`."
16
+ ]
17
+ },
18
+ {
19
+ "cell_type": "code",
20
+ "execution_count": null,
21
+ "metadata": {
22
+ "tags": [
23
+ "parameters"
24
+ ]
25
+ },
26
+ "outputs": [],
27
+ "source": [
28
+ "# Parameters\n",
29
+ "source_section = None\n",
30
+ "batch_size = 5000"
31
+ ]
32
+ },
33
+ {
34
+ "cell_type": "code",
35
+ "execution_count": null,
36
+ "metadata": {},
37
+ "outputs": [],
38
+ "source": [
39
+ "import sys, warnings, re, json, time\n",
40
+ "sys.path.insert(0, '/opt/epstein_env/research')\n",
41
+ "warnings.filterwarnings('ignore')\n",
42
+ "\n",
43
+ "import fitz\n",
44
+ "import pandas as pd\n",
45
+ "from pathlib import Path\n",
46
+ "\n",
47
+ "from research_lib.config import RAW_DIR, COLLECTIONS\n",
48
+ "from research_lib.db import fetch_df, upsert_feature\n",
49
+ "from research_lib.incremental import (\n",
50
+ " start_run, finish_run, get_unprocessed_documents,\n",
51
+ ")\n",
52
+ "\n",
53
+ "print('Libraries loaded.')"
54
+ ]
55
+ },
56
+ {
57
+ "cell_type": "code",
58
+ "execution_count": null,
59
+ "metadata": {},
60
+ "outputs": [],
61
+ "source": [
62
+ "# ---- Classification stamp patterns ----\n",
63
+ "STAMP_PATTERNS = [\n",
64
+ " 'CONFIDENTIAL',\n",
65
+ " 'CLASSIFIED',\n",
66
+ " 'SEALED',\n",
67
+ " 'TOP SECRET',\n",
68
+ " 'RESTRICTED',\n",
69
+ " 'UNCLASSIFIED',\n",
70
+ " 'DECLASSIFIED',\n",
71
+ " 'FOR OFFICIAL USE ONLY',\n",
72
+ "]\n",
73
+ "\n",
74
+ "# Compile combined regex for efficiency\n",
75
+ "_stamp_re = re.compile(\n",
76
+ " r'\\b(' + '|'.join(re.escape(s) for s in STAMP_PATTERNS) + r')\\b',\n",
77
+ " re.IGNORECASE,\n",
78
+ ")\n",
79
+ "\n",
80
+ "\n",
81
+ "def extract_pdf_metadata(pdf_path):\n",
82
+ " \"\"\"Extract metadata dict from a PDF file.\"\"\"\n",
83
+ " try:\n",
84
+ " doc = fitz.open(pdf_path)\n",
85
+ " meta = doc.metadata or {}\n",
86
+ " result = {\n",
87
+ " 'author': meta.get('author', ''),\n",
88
+ " 'title': meta.get('title', ''),\n",
89
+ " 'subject': meta.get('subject', ''),\n",
90
+ " 'creator': meta.get('creator', ''),\n",
91
+ " 'producer': meta.get('producer', ''),\n",
92
+ " 'creationDate': meta.get('creationDate', ''),\n",
93
+ " 'modDate': meta.get('modDate', ''),\n",
94
+ " }\n",
95
+ " doc.close()\n",
96
+ " return result\n",
97
+ " except Exception:\n",
98
+ " return {}\n",
99
+ "\n",
100
+ "\n",
101
+ "def scan_for_stamps(document_id):\n",
102
+ " \"\"\"Check the first page OCR text for classification stamps.\"\"\"\n",
103
+ " first_page = fetch_df(\n",
104
+ " 'SELECT ocr_text FROM pages WHERE document_id = %s ORDER BY page_number LIMIT 1',\n",
105
+ " [document_id],\n",
106
+ " )\n",
107
+ " if first_page.empty or not first_page.iloc[0]['ocr_text']:\n",
108
+ " return []\n",
109
+ " text = first_page.iloc[0]['ocr_text']\n",
110
+ " found = list(set(m.upper() for m in _stamp_re.findall(text)))\n",
111
+ " return sorted(found)\n",
112
+ "\n",
113
+ "\n",
114
+ "print('Functions defined.')"
115
+ ]
116
+ },
117
+ {
118
+ "cell_type": "code",
119
+ "execution_count": null,
120
+ "metadata": {},
121
+ "outputs": [],
122
+ "source": [
123
+ "# ---- Identify unprocessed documents ----\n",
124
+ "PIPELINE = 'metadata_extraction'\n",
125
+ "run_id = start_run(PIPELINE, source_section=source_section, parameters={\n",
126
+ " 'batch_size': batch_size,\n",
127
+ "})\n",
128
+ "\n",
129
+ "docs_df = get_unprocessed_documents(\n",
130
+ " PIPELINE, source_section=source_section,\n",
131
+ " feature_table='document_features', feature_name='pdf_metadata',\n",
132
+ ")\n",
133
+ "print(f'Documents to process: {len(docs_df)}')\n",
134
+ "if len(docs_df) > 0:\n",
135
+ " print(docs_df['source_section'].value_counts().to_string())"
136
+ ]
137
+ },
138
+ {
139
+ "cell_type": "code",
140
+ "execution_count": null,
141
+ "metadata": {},
142
+ "outputs": [],
143
+ "source": [
144
+ "# ---- Process in batches ----\n",
145
+ "total_processed = 0\n",
146
+ "stamp_counter = {s: 0 for s in STAMP_PATTERNS}\n",
147
+ "meta_coverage = {'has_author': 0, 'has_title': 0, 'has_date': 0}\n",
148
+ "total_batches = (len(docs_df) + batch_size - 1) // batch_size\n",
149
+ "\n",
150
+ "for batch_idx in range(total_batches):\n",
151
+ " start = batch_idx * batch_size\n",
152
+ " end = min(start + batch_size, len(docs_df))\n",
153
+ " batch = docs_df.iloc[start:end]\n",
154
+ " print(f'\\nBatch {batch_idx + 1}/{total_batches}: documents {start}-{end - 1}')\n",
155
+ "\n",
156
+ " t0 = time.time()\n",
157
+ " meta_rows = []\n",
158
+ " stamp_rows = []\n",
159
+ "\n",
160
+ " for _, row in batch.iterrows():\n",
161
+ " doc_id = int(row['id'])\n",
162
+ "\n",
163
+ " # Extract PDF metadata\n",
164
+ " pdf_path = Path(row.get('file_path', '')) if row.get('file_path') else None\n",
165
+ " if pdf_path is None or not pdf_path.exists():\n",
166
+ " pdf_path = RAW_DIR / row['source_section'] / row['filename']\n",
167
+ "\n",
168
+ " meta = extract_pdf_metadata(str(pdf_path)) if pdf_path.exists() else {}\n",
169
+ " meta_rows.append((doc_id, 'pdf_metadata', None, json.dumps(meta)))\n",
170
+ "\n",
171
+ " # Track coverage\n",
172
+ " if meta.get('author'):\n",
173
+ " meta_coverage['has_author'] += 1\n",
174
+ " if meta.get('title'):\n",
175
+ " meta_coverage['has_title'] += 1\n",
176
+ " if meta.get('creationDate') or meta.get('modDate'):\n",
177
+ " meta_coverage['has_date'] += 1\n",
178
+ "\n",
179
+ " # Scan for classification stamps\n",
180
+ " stamps = scan_for_stamps(doc_id)\n",
181
+ " stamp_rows.append((doc_id, 'classification_stamps', None, json.dumps(stamps)))\n",
182
+ " for s in stamps:\n",
183
+ " if s in stamp_counter:\n",
184
+ " stamp_counter[s] += 1\n",
185
+ "\n",
186
+ " # Upsert features\n",
187
+ " n_meta = upsert_feature(\n",
188
+ " 'document_features',\n",
189
+ " ['document_id', 'feature_name'],\n",
190
+ " ['feature_value', 'feature_json'],\n",
191
+ " meta_rows,\n",
192
+ " )\n",
193
+ " n_stamp = upsert_feature(\n",
194
+ " 'document_features',\n",
195
+ " ['document_id', 'feature_name'],\n",
196
+ " ['feature_value', 'feature_json'],\n",
197
+ " stamp_rows,\n",
198
+ " )\n",
199
+ " elapsed = time.time() - t0\n",
200
+ " print(f' {n_meta} metadata + {n_stamp} stamp rows in {elapsed:.1f}s')\n",
201
+ " total_processed += len(batch)\n",
202
+ "\n",
203
+ "finish_run(run_id, documents_processed=total_processed)\n",
204
+ "print(f'\\nRun {run_id} complete: {total_processed} documents processed.')"
205
+ ]
206
+ },
207
+ {
208
+ "cell_type": "code",
209
+ "execution_count": null,
210
+ "metadata": {},
211
+ "outputs": [],
212
+ "source": [
213
+ "# ---- Summary: metadata coverage ----\n",
214
+ "total = max(total_processed, 1)\n",
215
+ "print('Metadata Coverage:')\n",
216
+ "for field, count in meta_coverage.items():\n",
217
+ " print(f' {field}: {count} / {total} ({count / total * 100:.1f}%)')\n",
218
+ "\n",
219
+ "print('\\nClassification Stamp Frequency:')\n",
220
+ "for stamp, count in sorted(stamp_counter.items(), key=lambda x: -x[1]):\n",
221
+ " if count > 0:\n",
222
+ " print(f' {stamp}: {count}')\n",
223
+ "\n",
224
+ "if all(v == 0 for v in stamp_counter.values()):\n",
225
+ " print(' No classification stamps found.')"
226
+ ]
227
+ }
228
+ ],
229
+ "metadata": {
230
+ "kernelspec": {
231
+ "display_name": "Python 3",
232
+ "language": "python",
233
+ "name": "python3"
234
+ },
235
+ "language_info": {
236
+ "name": "python",
237
+ "version": "3.10.0"
238
+ }
239
+ },
240
+ "nbformat": 4,
241
+ "nbformat_minor": 5
242
+ }