dseditor commited on
Commit
a355cd9
·
verified ·
1 Parent(s): e966ea4
Files changed (3) hide show
  1. app.py +487 -0
  2. readme.md +64 -0
  3. requirements.txt +5 -0
app.py ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ import fitz # PyMuPDF
7
+ import re
8
+ from typing import Dict, List, Optional
9
+ import tempfile
10
+ import unicodedata
11
+
12
+ def clean_text(text: str) -> str:
13
+ """清理文本,移除多餘空白和特殊字符"""
14
+ if not text:
15
+ return ""
16
+ # 正規化Unicode字符
17
+ text = unicodedata.normalize('NFKC', text)
18
+ # 移除多餘的空白字符
19
+ text = re.sub(r'\s+', ' ', text.strip())
20
+ return text
21
+
22
+ def extract_title_from_collection(collection_name: str) -> str:
23
+ """從論文集名稱中提取簡潔的標題"""
24
+ # 移除常見的論文集關鍵詞
25
+ keywords_to_remove = ['研究', '論文集', '期刊', '學報', '彙編', '全刊', '下載']
26
+
27
+ title = collection_name
28
+ for keyword in keywords_to_remove:
29
+ title = title.replace(keyword, '')
30
+
31
+ # 移除數字和特殊符號,保留核心名稱
32
+ title = re.sub(r'\d+', '', title)
33
+ title = re.sub(r'[^\w\u4e00-\u9fff]', '', title)
34
+
35
+ return clean_text(title) if title.strip() else collection_name
36
+
37
+ def download_pdf(url: str) -> Optional[str]:
38
+ """下載PDF文件並返回臨時文件路徑"""
39
+ try:
40
+ headers = {
41
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
42
+ }
43
+ response = requests.get(url, headers=headers, stream=True, timeout=30)
44
+ response.raise_for_status()
45
+
46
+ # 創建臨時文件
47
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf')
48
+ temp_file.write(response.content)
49
+ temp_file.close()
50
+
51
+ return temp_file.name
52
+ except Exception as e:
53
+ print(f"下載PDF失敗: {e}")
54
+ return None
55
+
56
+ def extract_pdf_content(pdf_path: str) -> Dict[str, str]:
57
+ """從PDF中提取標題、作者和摘要"""
58
+ try:
59
+ doc = fitz.open(pdf_path)
60
+
61
+ # 提取前幾頁的文本
62
+ full_text = ""
63
+ for page_num in range(min(3, len(doc))): # 只處理前3頁
64
+ page = doc[page_num]
65
+ text = page.get_text()
66
+ full_text += text + "\n"
67
+
68
+ doc.close()
69
+
70
+ # 清理文本
71
+ full_text = clean_text(full_text)
72
+
73
+ # 提取標題(通常在文檔開頭,字體較大)
74
+ title = extract_title(full_text)
75
+
76
+ # 提取作者
77
+ author = extract_author(full_text)
78
+
79
+ # 提取摘要
80
+ abstract = extract_abstract(full_text)
81
+
82
+ return {
83
+ "title": title,
84
+ "author": author,
85
+ "abstract": abstract
86
+ }
87
+ except Exception as e:
88
+ print(f"提取PDF內容失敗: {e}")
89
+ return {"title": "", "author": "", "abstract": ""}
90
+
91
+ def extract_title(text: str) -> str:
92
+ """提取標題"""
93
+ lines = text.split('\n')
94
+
95
+ # 尋找可能的標題(通常在前幾行,不包含常見的頁眉頁腳詞彙)
96
+ skip_keywords = ['頁', 'page', '目錄', '內容', '摘要', 'abstract', '關鍵詞']
97
+
98
+ for line in lines[:20]: # 檢查前20行
99
+ line = line.strip()
100
+ if len(line) > 5 and len(line) < 100: # 標題長度合理
101
+ if not any(keyword in line.lower() for keyword in skip_keywords):
102
+ if not re.match(r'^\d+\.?\d*$', line): # 不是純數字
103
+ return line
104
+
105
+ # 如果沒找到合適標題,返回第一行非空內容
106
+ for line in lines:
107
+ line = line.strip()
108
+ if line and len(line) > 3:
109
+ return line[:80] # 限制長度
110
+
111
+ return "未知標題"
112
+
113
+ def extract_author(text: str) -> str:
114
+ """提取作者"""
115
+ # 常見的作者指示詞
116
+ author_patterns = [
117
+ r'作者[::]\s*([^\n]+)',
118
+ r'著者[::]\s*([^\n]+)',
119
+ r'by\s+([^\n]+)',
120
+ r'撰稿[::]\s*([^\n]+)',
121
+ ]
122
+
123
+ for pattern in author_patterns:
124
+ match = re.search(pattern, text, re.IGNORECASE)
125
+ if match:
126
+ author = clean_text(match.group(1))
127
+ return author[:50] # 限制長度
128
+
129
+ # 如果沒找到明確的作者標識,尋找人名模式
130
+ # 中文人名模式(2-4個中文字符)
131
+ chinese_name_pattern = r'[\u4e00-\u9fff]{2,4}(?:\s*、\s*[\u4e00-\u9fff]{2,4})*'
132
+
133
+ lines = text.split('\n')
134
+ for line in lines[:30]: # 檢查前30行
135
+ line = line.strip()
136
+ if len(line) < 100: # 作者行通常不會太長
137
+ matches = re.findall(chinese_name_pattern, line)
138
+ if matches and len(line) < 50:
139
+ return line
140
+
141
+ return "未知作者"
142
+
143
+ def extract_abstract(text: str) -> str:
144
+ """提取摘要"""
145
+ # 摘要關鍵詞
146
+ abstract_keywords = ['摘要', 'abstract', '內容摘要', '研究摘要']
147
+
148
+ text_lines = text.split('\n')
149
+ abstract_start = -1
150
+
151
+ # 尋找摘要開始位置
152
+ for i, line in enumerate(text_lines):
153
+ line_lower = line.lower().strip()
154
+ if any(keyword in line_lower for keyword in abstract_keywords):
155
+ abstract_start = i
156
+ break
157
+
158
+ if abstract_start == -1:
159
+ # 如果沒找到明確的摘要標識,取文檔開頭部分作為摘要
160
+ abstract_text = ' '.join(text_lines[5:15]) # 取中間部分
161
+ else:
162
+ # 從摘要標識處開始提取
163
+ abstract_lines = text_lines[abstract_start+1:abstract_start+15] # 取摘要後的內容
164
+ abstract_text = ' '.join(abstract_lines)
165
+
166
+ # 清理和限制摘要長度
167
+ abstract_text = clean_text(abstract_text)
168
+ if len(abstract_text) > 500:
169
+ abstract_text = abstract_text[:500] + "..."
170
+
171
+ return abstract_text if abstract_text else "無摘要資訊"
172
+
173
+ def process_json_data(json_input: str) -> str:
174
+ """處理JSON數據,補充缺失欄位"""
175
+ try:
176
+ # 解析輸入的JSON
177
+ data = json.loads(json_input)
178
+
179
+ if not isinstance(data, list):
180
+ data = [data]
181
+
182
+ processed_data = []
183
+
184
+ for item in data:
185
+ # 獲取現有資料
186
+ collection_name = item.get("論文集名稱", "")
187
+ author = item.get("作者", "")
188
+ download_url = item.get("下載位置", "")
189
+
190
+ # 初始化處理後的項目
191
+ processed_item = {
192
+ "論文集名稱": collection_name,
193
+ "作者": author,
194
+ "下載位置": download_url,
195
+ "名稱": "",
196
+ "摘要": ""
197
+ }
198
+
199
+ # 如果有下載位置,嘗試下載並提取資訊
200
+ if download_url:
201
+ print(f"正在處理: {collection_name}")
202
+ pdf_path = download_pdf(download_url)
203
+
204
+ if pdf_path:
205
+ try:
206
+ # 從PDF提取資訊
207
+ extracted_info = extract_pdf_content(pdf_path)
208
+
209
+ # 設定名稱(避免與論文集名稱重複)
210
+ pdf_title = extracted_info.get("title", "")
211
+ if pdf_title and pdf_title != "未知標題":
212
+ # 確保名稱與論文集名稱不重複
213
+ collection_base = extract_title_from_collection(collection_name)
214
+ if collection_base not in pdf_title:
215
+ processed_item["名稱"] = pdf_title
216
+ else:
217
+ # 如果重複,使用更簡潔的版本
218
+ processed_item["名稱"] = pdf_title.replace(collection_base, "").strip()
219
+ else:
220
+ processed_item["名稱"] = extract_title_from_collection(collection_name)
221
+
222
+ # 更新作者(如果原來沒有或PDF中有更詳細的資訊)
223
+ pdf_author = extracted_info.get("author", "")
224
+ if pdf_author and pdf_author != "未知作者":
225
+ if not author or author == "犯罪防治研究中心彙編":
226
+ processed_item["作者"] = pdf_author
227
+
228
+ # 設定摘要
229
+ processed_item["摘要"] = extracted_info.get("abstract", "無摘要資訊")
230
+
231
+ finally:
232
+ # 清理臨時文件
233
+ if os.path.exists(pdf_path):
234
+ os.unlink(pdf_path)
235
+ else:
236
+ # 如果無法下載PDF,使用現有資訊
237
+ processed_item["名稱"] = extract_title_from_collection(collection_name)
238
+ processed_item["摘要"] = "無法獲取摘要資訊"
239
+ else:
240
+ # 如果沒有下載位置,使用現有資訊
241
+ processed_item["名稱"] = extract_title_from_collection(collection_name)
242
+ processed_item["摘要"] = "無下載位置,無法提取摘要"
243
+
244
+ processed_data.append(processed_item)
245
+
246
+ # 返回格式化的JSON
247
+ return json.dumps(processed_data, ensure_ascii=False, indent=2)
248
+
249
+ except Exception as e:
250
+ return f"處理錯誤: {str(e)}"
251
+
252
+ def save_json_file(json_data: str, filename: str) -> str:
253
+ """保存JSON文件"""
254
+ try:
255
+ if not filename:
256
+ filename = "processed_data.json"
257
+
258
+ if not filename.endswith('.json'):
259
+ filename += '.json'
260
+
261
+ # 確保文件名安全
262
+ filename = re.sub(r'[^\w\-_\.]', '_', filename)
263
+
264
+ with open(filename, 'w', encoding='utf-8') as f:
265
+ f.write(json_data)
266
+
267
+ return f"文件已保存: {filename}"
268
+ except Exception as e:
269
+ return f"保存失敗: {str(e)}"
270
+
271
+ # PDF網址處理函數
272
+ def process_pdf_urls(urls_text: str) -> str:
273
+ """處理PDF網址列表,直接提取資訊"""
274
+ try:
275
+ # 解析網址
276
+ urls = [url.strip() for url in urls_text.strip().split('\n') if url.strip()]
277
+
278
+ if not urls:
279
+ return "請輸入至少一個PDF網址"
280
+
281
+ processed_data = []
282
+
283
+ for i, url in enumerate(urls, 1):
284
+ print(f"正在處理第 {i}/{len(urls)} 個PDF: {url}")
285
+
286
+ # 下載PDF
287
+ pdf_path = download_pdf(url)
288
+
289
+ if pdf_path:
290
+ try:
291
+ # 從PDF提取資訊
292
+ extracted_info = extract_pdf_content(pdf_path)
293
+
294
+ # 構建資料項目
295
+ item = {
296
+ "名稱": extracted_info.get("title", f"PDF文件 {i}"),
297
+ "作者": extracted_info.get("author", "未知作者"),
298
+ "摘要": extracted_info.get("abstract", "無摘要資訊"),
299
+ "下載位置": url,
300
+ "論文集名稱": f"直接處理PDF {i}"
301
+ }
302
+
303
+ processed_data.append(item)
304
+
305
+ finally:
306
+ # 清理臨時文件
307
+ if os.path.exists(pdf_path):
308
+ os.unlink(pdf_path)
309
+ else:
310
+ # PDF下載失敗時的處理
311
+ item = {
312
+ "名稱": f"無法下載的PDF {i}",
313
+ "作者": "未知作者",
314
+ "摘要": "PDF下載失敗,無法提取摘要",
315
+ "下載位置": url,
316
+ "論文集名稱": f"處理失敗 {i}"
317
+ }
318
+ processed_data.append(item)
319
+
320
+ # 返回格式化的JSON
321
+ return json.dumps(processed_data, ensure_ascii=False, indent=2)
322
+
323
+ except Exception as e:
324
+ return f"處理錯誤: {str(e)}"
325
+
326
+ # Gradio界面
327
+ with gr.Blocks(title="PDF資料處理器", theme=gr.themes.Soft()) as demo:
328
+ gr.Markdown("""
329
+ # PDF論文資料自動處理系統
330
+
331
+ 此系統可以:
332
+ 1. 從JSON中讀取論文資訊並補充缺失欄位
333
+ 2. 直接處理PDF網址列表
334
+ 3. 自動下載PDF文件並提取內容
335
+ 4. 提取標題、作者、摘要等資訊
336
+ 5. 生成完整的JSON資料
337
+ 6. 保存處理後的文件
338
+ """)
339
+
340
+ with gr.Tabs():
341
+ # JSON處理標籤頁
342
+ with gr.TabItem("JSON資料處理"):
343
+ with gr.Row():
344
+ with gr.Column(scale=1):
345
+ json_input = gr.Textbox(
346
+ label="輸入JSON資料",
347
+ placeholder="請貼上您的JSON資料...",
348
+ lines=10,
349
+ value='[\n {\n "論文集名稱": "刑事政策與犯罪防治研究36",\n "作者": "犯罪防治研究中心彙編",\n "下載位置": "https://www.cprc.moj.gov.tw/media/20213330/3_36%E6%9C%9F%E5%85%A8%E5%88%8A%E4%B8%8B%E8%BC%89.pdf?mediaDL=true"\n }\n]'
350
+ )
351
+
352
+ filename_input1 = gr.Textbox(
353
+ label="保存文件名",
354
+ placeholder="例: processed_papers.json",
355
+ value="processed_papers.json"
356
+ )
357
+
358
+ process_json_btn = gr.Button("處理JSON資料", variant="primary", size="lg")
359
+
360
+ with gr.Column(scale=2):
361
+ output_json1 = gr.Textbox(
362
+ label="處理結果",
363
+ lines=20,
364
+ show_copy_button=True
365
+ )
366
+
367
+ save_status1 = gr.Textbox(
368
+ label="保存狀態",
369
+ lines=2
370
+ )
371
+
372
+ download_file1 = gr.File(
373
+ label="下載處理後的文件",
374
+ visible=False
375
+ )
376
+
377
+ # PDF網址處理標籤頁
378
+ with gr.TabItem("PDF網址直接處理"):
379
+ with gr.Row():
380
+ with gr.Column(scale=1):
381
+ pdf_urls_input = gr.Textbox(
382
+ label="輸入PDF網址",
383
+ placeholder="請輸入PDF網址,每行一個...\n\n例如:\nhttps://example.com/paper1.pdf\nhttps://example.com/paper2.pdf\nhttps://example.com/paper3.pdf",
384
+ lines=12,
385
+ value="https://www.cprc.moj.gov.tw/media/20213330/3_36%E6%9C%9F%E5%85%A8%E5%88%8A%E4%B8%8B%E8%BC%89.pdf?mediaDL=true"
386
+ )
387
+
388
+ filename_input2 = gr.Textbox(
389
+ label="保存文件名",
390
+ placeholder="例: pdf_extracted_data.json",
391
+ value="pdf_extracted_data.json"
392
+ )
393
+
394
+ process_urls_btn = gr.Button("處理PDF網址", variant="primary", size="lg")
395
+
396
+ with gr.Column(scale=2):
397
+ output_json2 = gr.Textbox(
398
+ label="處理結果",
399
+ lines=20,
400
+ show_copy_button=True
401
+ )
402
+
403
+ save_status2 = gr.Textbox(
404
+ label="保存狀態",
405
+ lines=2
406
+ )
407
+
408
+ download_file2 = gr.File(
409
+ label="下載處理後的文件",
410
+ visible=False
411
+ )
412
+
413
+ def process_and_save_json(json_input, filename):
414
+ # 處理JSON資料
415
+ result = process_json_data(json_input)
416
+
417
+ # 保存文件
418
+ save_msg = save_json_file(result, filename)
419
+
420
+ # 如果保存成功,提供下載
421
+ if "已保存" in save_msg:
422
+ return result, save_msg, gr.update(visible=True, value=filename)
423
+ else:
424
+ return result, save_msg, gr.update(visible=False)
425
+
426
+ def process_and_save_urls(urls_input, filename):
427
+ # 處理PDF網址
428
+ result = process_pdf_urls(urls_input)
429
+
430
+ # 保存文件
431
+ save_msg = save_json_file(result, filename)
432
+
433
+ # 如果保存成功,提供下載
434
+ if "已保存" in save_msg:
435
+ return result, save_msg, gr.update(visible=True, value=filename)
436
+ else:
437
+ return result, save_msg, gr.update(visible=False)
438
+
439
+ # JSON處理按鈕事件
440
+ process_json_btn.click(
441
+ process_and_save_json,
442
+ inputs=[json_input, filename_input1],
443
+ outputs=[output_json1, save_status1, download_file1]
444
+ )
445
+
446
+ # PDF網址處理按鈕事件
447
+ process_urls_btn.click(
448
+ process_and_save_urls,
449
+ inputs=[pdf_urls_input, filename_input2],
450
+ outputs=[output_json2, save_status2, download_file2]
451
+ )
452
+
453
+ gr.Markdown("""
454
+ ## 使用說明:
455
+
456
+ ### JSON資料處理模式:
457
+ 1. 將現有的JSON資料貼入文本框
458
+ 2. 系統會根據下載位置自動獲取PDF並補充缺失欄位
459
+ 3. 適合處理已有部分資訊的資料集
460
+
461
+ ### PDF網址直接處理模式:
462
+ 1. 直接貼入PDF網址,每行一個
463
+ 2. 系統會自動下載並提取完整資訊
464
+ 3. 適合批量處理新的PDF文件
465
+
466
+ ### 通用功能:
467
+ - 自動提取標題、作者、摘要
468
+ - 避免重複內容
469
+ - 生成標準化JSON格式
470
+ - 支援文件下載
471
+
472
+ ## 注意事項:
473
+ - 處理時間取決於PDF文件大小和數量
474
+ - 系統會自動清理重複的標題內容
475
+ - 如果PDF無法下載,會使用預設資訊
476
+ - 建議分批處理大量文件以獲得最佳效果
477
+
478
+ ## 範例PDF網址格式:
479
+ ```
480
+ https://example.com/paper1.pdf
481
+ https://example.com/paper2.pdf
482
+ https://example.com/paper3.pdf
483
+ ```
484
+ """)
485
+
486
+ if __name__ == "__main__":
487
+ demo.launch()
readme.md ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PDF論文資料自動處理系統
2
+
3
+ 這是一個自動化處理學術論文PDF文件的Hugging Face Space應用程序,能夠從JSON數據中提取論文資訊,自動下載PDF文件,並提取完整的元數據。
4
+
5
+ ## 功能特色
6
+
7
+ - 📄 **PDF自動下載**: 根據JSON中的下載連結自動獲取PDF文件
8
+ - 🔍 **智能內容提取**: 使用PyMuPDF提取標題、作者、摘要等關鍵資訊
9
+ - 🚫 **重複內容處理**: 自動識別並避免名稱與論文集名稱的重複
10
+ - 💾 **JSON格式化輸出**: 生成結構化的完整JSON資料
11
+ - 📁 **文件保存功能**: 支持自定義文件名保存處理結果
12
+
13
+ ## 使用方法
14
+
15
+ 1. **準備JSON數據**: 包含論文集名稱、作者、下載位置等基本資訊
16
+ 2. **輸入處理**: 將JSON貼入應用界面
17
+ 3. **自動處理**: 系統自動下載PDF並提取缺失資訊
18
+ 4. **結果輸出**: 獲得包含完整欄位的JSON數據
19
+
20
+ ## 輸入格式
21
+
22
+ ```json
23
+ [
24
+ {
25
+ "論文集名稱": "刑事政策與犯罪防治研究36",
26
+ "作者": "犯罪防治研究中心彙編",
27
+ "下載位置": "https://example.com/paper.pdf"
28
+ }
29
+ ]
30
+ ```
31
+
32
+ ## 輸出格式
33
+
34
+ ```json
35
+ [
36
+ {
37
+ "論文集名稱": "刑事政策與犯罪防治研究36",
38
+ "作者": "實際作者姓名",
39
+ "下載位置": "https://example.com/paper.pdf",
40
+ "名稱": "具體論文標題",
41
+ "摘要": "論文摘要內容..."
42
+ }
43
+ ]
44
+ ```
45
+
46
+ ## 技術特點
47
+
48
+ - **智能標題提取**: 避免與論文集名稱重複,提供更精準的論文標題
49
+ - **多語言支援**: 支援中英文內容的智能識別和處理
50
+ - **錯誤處理**: 完善的異常處理機制,確保處理穩定性
51
+ - **臨時文件管理**: 自動清理下載的臨時PDF文件
52
+
53
+ ## 依賴套件
54
+
55
+ - gradio: Web界面框架
56
+ - requests: HTTP請求處理
57
+ - PyMuPDF: PDF文件解析
58
+ - unicodedata2: 字符正規化處理
59
+
60
+ ## 注意事項
61
+
62
+ - 處理時間取決於PDF文件的大小和網絡狀況
63
+ - 系統會自動處理PDF下載失敗的情況
64
+ - 建議對大量文件分批處理以獲得最佳效果
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ requests>=2.31.0
3
+ PyMuPDF>=1.23.0
4
+ pathlib
5
+ unicodedata2