deneve07 commited on
Commit
7b13559
·
verified ·
1 Parent(s): 6de117e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +211 -266
app.py CHANGED
@@ -8,55 +8,90 @@ from playwright.sync_api import sync_playwright
8
  from curl_cffi import requests as curl_req
9
  import gradio as gr
10
 
 
 
 
 
11
  os.system("playwright install chromium")
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  def get_official_japanese_name(ingredient_en):
14
- """
15
- 利用維基百科 API 跨語言連結,精準取得官方日文藥名 (JAN)
16
- """
17
  try:
18
- # 1. 查詢英文維基百科,確認該藥品頁面是否存在
19
  search_url = f"https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch={quote(ingredient_en)}&utf8=&format=json"
20
  res = requests.get(search_url, timeout=5).json()
21
  if not res['query']['search']: return None
22
 
23
- # 取得最吻合的英文頁面標題
24
  en_title = res['query']['search'][0]['title']
25
-
26
- # 2. 查詢該頁面是否有對應的「日文版」頁面,並抓取日文標題
27
  lang_url = f"https://en.wikipedia.org/w/api.php?action=query&titles={quote(en_title)}&prop=langlinks&lllang=ja&format=json"
28
  lang_res = requests.get(lang_url, timeout=5).json()
29
 
30
  pages = lang_res['query']['pages']
31
  for page_id in pages:
32
  if 'langlinks' in pages[page_id]:
33
- # 日文維基百科的標題,99% 就是日本官方一般名 (JAN)
34
  ja_title = pages[page_id]['langlinks'][0]['*']
35
- # 清除有時維基百科標題會帶有的後綴,例如 "フィネレノン (化合物)"
36
  return ja_title.split(' ')[0].split('(')[0]
37
  except Exception:
38
  pass
39
  return None
40
 
41
  def translate_lang(text, target_lang):
42
- """
43
- 智能翻譯分發器
44
- """
45
- # 💡 針對日文,優先使用「醫學實體對照法」,這比 Google 翻譯準確一萬倍
46
  if target_lang == 'ja':
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  official_ja = get_official_japanese_name(text)
48
  if official_ja:
49
- return official_ja # 成功拿到官方名稱,直接回傳
 
50
 
51
- # 若不是日文(如德文),或是維基百科查不到,才退回使用 Google 翻譯做備用
52
  try:
53
  url = f"https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl={target_lang}&dt=t&q={quote(text)}"
54
  res = requests.get(url, timeout=5)
55
  if res.status_code == 200:
56
- return res.json()[0][0][0].strip()
 
 
57
  except Exception:
58
  pass
59
-
60
  return text
61
 
62
  def is_generic(brand_name, company_name, ingredient):
@@ -64,7 +99,6 @@ def is_generic(brand_name, company_name, ingredient):
64
  c_lower = company_name.lower()
65
  i_lower = ingredient.lower()
66
 
67
- # 💡 新增您在 FASS 發現的學名藥廠: tiefenbacher, cinfa, polpharma, pharmaceutical innovation...
68
  generic_keywords = [
69
  'sandoz', 'teva', 'apotex', 'ratiopharm', 'jamp', 'mint', 'pharmascience', 'sanis', 'sivem',
70
  'auro', 'glenmark', 'taro', 'marcan', 'nora', 'mantra', 'reddy', 'mepha', 'axapharm',
@@ -82,63 +116,21 @@ def is_generic(brand_name, company_name, ingredient):
82
  return False
83
 
84
  def clean_brand_name(raw_name):
85
- """
86
- 精準擷取商品名主體:
87
- 1. 遇到劑量數字 (\d+) 直接切斷
88
- 2. 遇到多國常見的劑型、包裝描述詞彙直接切斷
89
- """
90
- # 擴充了德文、法文、英文、日文的常見劑型與包裝詞彙
91
- # 加入 inj (注射), pen (注射筆), prefilled (預填) 來應對 Semaglutide 等生物製劑
92
  pattern = r'(皮下注|錠|カプセル|顆粒|シロップ|OD|細粒|液|Augentropfen|Schmelztabletten|Tabletten|kids|Lingual|Sol|cp|inj|pen|prefilled|flex|\d+)'
93
-
94
- # 透過正則表達式忽略大小寫進行切割,並只取最前面的主商標名
95
  cleaned = re.split(pattern, raw_name, flags=re.IGNORECASE)[0]
96
-
97
- # 清除 ®、™ 符號,並去除頭尾可能殘留的空白或連接號
98
  return cleaned.replace('®', '').replace('™', '').strip(' -_')
99
 
 
100
  # ==========================================
101
- # 🚀 模組 A:使用 curl_cffi 抓取 (美、法、、瑞典)
102
  # ==========================================
103
- def get_usa_originator(ingredient):
104
- log, brands, companies = [], set(), set()
105
- try:
106
- session = curl_req.Session(impersonate="chrome120")
107
- log.append("1. 發送 GET 請求至 FDA Orange Book...")
108
- # 改用標準 GET 參數直接查詢結果頁面
109
- url = f"https://www.accessdata.fda.gov/scripts/cder/ob/results_product.cfm?Generic_Name={quote(ingredient)}&rx_otc=All"
110
- res = session.get(url, timeout=30, verify=False)
111
- log.append(f" -> 狀態碼: {res.status_code}, 內容長度: {len(res.text)}")
112
-
113
- soup = BeautifulSoup(res.text, 'html.parser')
114
- table = soup.find('table', id='example')
115
- if table:
116
- rows = table.find('tbody').find_all('tr') if table.find('tbody') else table.find_all('tr')
117
- log.append(f"2. 找到結果表格,共 {len(rows)} 列資料。")
118
- headers = [th.get_text(strip=True).lower() for th in table.find_all('th')]
119
- brand_idx = next((i for i, h in enumerate(headers) if 'proprietary name' in h), 2)
120
- rld_idx = next((i for i, h in enumerate(headers) if 'rld' in h), 8)
121
- mfg_idx = next((i for i, h in enumerate(headers) if 'applicant holder' in h), 10)
122
-
123
- for tr in rows:
124
- tds = tr.find_all('td')
125
- if len(tds) > max(rld_idx, brand_idx) and "RLD" in tds[rld_idx].get_text(strip=True).upper():
126
- brands.add(tds[brand_idx].get_text(strip=True))
127
- if len(tds) > mfg_idx: companies.add(tds[mfg_idx].get_text(strip=True))
128
- else:
129
- log.append("❌ HTML 中未發現 table#example,可能是無此藥或被阻擋。")
130
-
131
- if brands: return ", ".join(brands), ", ".join(companies), "\n".join(log)
132
- return "查無原廠", "-", "\n".join(log)
133
- except Exception as e: return "執行失敗", "-", "\n".join(log) + f"\n錯誤: {str(e)}"
134
-
135
  def get_australia_originator(ingredient):
136
  log, brands = [], set()
137
  try:
138
  session = curl_req.Session(impersonate="chrome120")
139
  log.append("1. 發送 GET 至澳洲 TGA 搜尋頁面...")
140
  res = session.get(f"https://www.tga.gov.au/resources/artg?keywords={ingredient}", timeout=30, verify=False)
141
- log.append(f" -> 狀態碼: {res.status_code}")
142
  soup = BeautifulSoup(res.text, 'html.parser')
143
 
144
  articles = soup.find_all('article', class_='node--artg')
@@ -152,78 +144,189 @@ def get_australia_originator(ingredient):
152
  if len(parts) > 1 and parts[0].strip():
153
  brand = parts[0].strip()
154
  if not is_generic(brand, "", ingredient):
155
- # 💡 取得該筆紀錄的內頁連結 (href)
156
  a_tag = title_tag.find('a')
157
  href = a_tag['href'] if a_tag else None
158
  cands.append({"brand": brand, "date": time_tag.get('datetime'), "href": href})
159
 
160
  if cands:
161
- # 依日期排序,找出最古老的註冊紀錄
162
  cands = sorted(cands, key=lambda x: x['date'])
163
  target = cands[0]
164
  brand = target['brand']
165
  log.append(f"✅ 依日期排序鎖定最早註冊: {target['date'][:10]} ({brand})")
166
 
167
- company = "TGA資料庫" # 預設值
168
-
169
- # 💡 新增:進入內頁抓取藥廠 (Sponsor)
170
  if target['href']:
171
- # 確保網址是絕對路徑
172
  detail_url = f"https://www.tga.gov.au{target['href']}" if target['href'].startswith('/') else target['href']
173
  log.append("3. 進入內頁抓取 Sponsor 藥廠資訊...")
174
-
175
  res_detail = session.get(detail_url, timeout=30, verify=False)
176
  if res_detail.status_code == 200:
177
  detail_soup = BeautifulSoup(res_detail.text, 'html.parser')
178
-
179
- # 依據您提供的 HTML 結構精準定位
180
  sponsor_div = detail_soup.find('div', class_=re.compile(r'field--name-field-sponsor'))
181
  if sponsor_div and sponsor_div.find('a'):
182
  company = sponsor_div.find('a').get_text(strip=True)
183
  log.append(f" -> 成功取得藥廠名稱: {company}")
184
- else:
185
- log.append(" -> 內頁未找到 Sponsor 標籤。")
186
- else:
187
- log.append(f" -> 進入內頁失敗,狀態碼: {res_detail.status_code}")
188
-
189
  return brand, company, "\n".join(log)
190
-
191
  return "查無原廠", "-", "\n".join(log)
192
  except Exception as e:
193
  return "執行失敗", "-", "\n".join(log) + f"\n錯誤: {str(e)}"
194
 
 
195
  # ==========================================
196
- # 🚀 模組 B:使用 Playwright 抓取 (比、英、加、日、德、瑞)
197
  # ==========================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  def get_belgium_originator(ingredient, page):
199
  log, brands = [], set()
200
  try:
201
  log.append(f"1. 前往 CBIP (比利時) 搜尋頁面 (搜尋: {ingredient})...")
202
  page.goto(f"https://www.cbip.be/fr/search?q={ingredient}", timeout=45000, wait_until="domcontentloaded")
203
-
204
- # 等待搜尋結果區塊渲染
205
  page.wait_for_selector('ul.search-results, .no-results', timeout=15000)
206
  page.wait_for_timeout(2000)
207
 
208
  soup = BeautifulSoup(page.content(), 'html.parser')
209
-
210
- # 💡 修正:直接抓取搜尋結果清單中的所有連結,不再嚴格限制 href 的結構
211
- log.append("2. 掃描搜尋結果...")
212
  results_list = soup.find('ul', class_=re.compile(r'search-results|results'))
213
 
214
  if results_list:
215
  links = results_list.find_all('a')
216
  for link in links:
217
  text = link.get_text(strip=True)
218
-
219
- # 過濾掉太短的字或明顯不是藥名的系統文字
220
  if len(text) > 3 and not text.lower().startswith('voir'):
221
- # 依賴強大的黑名單過濾學名藥廠 (如 EG, Teva, Sandoz)
222
  if not is_generic(text, "", ingredient):
223
  brands.add(clean_brand_name(text))
224
- else:
225
- log.append("❌ 畫面上未出現搜尋結果清單。")
226
-
227
  if brands: return ", ".join(brands), "CBIP 資料庫", "\n".join(log)
228
  return "查無原廠", "-", "\n".join(log) + "\n❌ 查無資料或全為學名藥"
229
  except Exception as e:
@@ -237,7 +340,6 @@ def get_uk_originator(ingredient, page):
237
  page.wait_for_selector('.search-results-product-info-title-link', timeout=15000)
238
  soup = BeautifulSoup(page.content(), 'html.parser')
239
  links = soup.find_all('a', class_='search-results-product-info-title-link')
240
- log.append(f"2. 找到 {len(links)} 筆標題,開始篩選。")
241
  for link in links:
242
  title = link.get_text(strip=True)
243
  if not title.lower().startswith(ingredient.lower()):
@@ -256,18 +358,12 @@ def get_canada_originator(ingredient, page):
256
  page.goto("https://health-products.canada.ca/dpd-bdpp/index-eng.jsp", timeout=45000, wait_until="domcontentloaded")
257
  page.locator('input[id="activeIngredient"]').fill(ingredient)
258
  page.keyboard.press("Enter")
259
-
260
- log.append("2. 等待結果表格或查無資料訊息...")
261
  page.wait_for_selector('table#results, .alert-info, .alert-warning', timeout=15000)
262
  soup = BeautifulSoup(page.content(), 'html.parser')
263
  table = soup.find('table', id='results')
264
 
265
- if not table or not table.find('tbody'):
266
- log.append("❌ 畫面上未出現表格,可能查無此藥。")
267
- return "查無資料", "-", "\n".join(log)
268
-
269
  rows = table.find('tbody').find_all('tr')
270
- log.append(f"3. 找到 {len(rows)} 筆資料,進行黑名單過濾。")
271
  all_cands = []
272
  for tr in rows:
273
  tds = tr.find_all('td')
@@ -279,12 +375,10 @@ def get_canada_originator(ingredient, page):
279
  all_cands.append({"company": comp, "product": tds[3].get_text(strip=True), "din": int(m.group())})
280
 
281
  if not all_cands: return "查無原廠", "-", "\n".join(log) + "\n❌ 剩餘皆為學名藥廠"
282
-
283
  all_cands = sorted(all_cands, key=lambda x: x['din'])
284
  orig_comp = all_cands[0]['company']
285
  brands = set([c['product'] for c in all_cands if c['company'] == orig_comp])
286
- log.append(f"✅ DIN 鎖定最古老藥廠: {orig_comp}")
287
- return ", ".join(brands), orig_comp, "\n".join(log)
288
  except Exception as e: return "執行失敗", "-", "\n".join(log) + f"\n錯誤: {str(e)}"
289
 
290
  def get_japan_originator(ing_ja, page):
@@ -292,79 +386,55 @@ def get_japan_originator(ing_ja, page):
292
  try:
293
  log.append(f"1. 前往 PMDA (搜尋: {ing_ja})...")
294
  page.goto("https://www.pmda.go.jp/PmdaSearch/iyakuSearch/", timeout=45000, wait_until="domcontentloaded")
295
-
296
- log.append("2. 準確定位置首頁搜尋框 (id='txtName')...")
297
- # 依照您提供的 HTML 結構,直接鎖定 txtName
298
  search_input = page.locator('input#txtName')
299
  search_input.wait_for(state="attached", timeout=15000)
300
  search_input.fill(ing_ja, force=True)
301
 
302
- log.append("3. 觸發搜尋並監聽彈出新視窗...")
303
  with page.expect_popup() as popup_info:
304
  search_input.press("Enter")
305
  popup = popup_info.value
306
-
307
- log.append("4. 等待新視窗表格並啟動自動翻頁掃描...")
308
  current_page = 1
309
 
310
  while True:
311
- # 等待表格出現
312
  popup.wait_for_selector('table#ResultList, .errormsg, .non-result', timeout=15000)
313
  soup = BeautifulSoup(popup.content(), 'html.parser')
314
  table = soup.find('table', id='ResultList')
315
 
316
  if table:
317
  rows = table.find_all('tr')
318
- log.append(f" -> 第 {current_page} 頁找到 {len(rows)} 列資料。")
319
  for tr in rows:
320
  tds = tr.find_all('td')
321
  if len(tds) >= 3:
322
  title = tds[1].get_text(strip=True)
323
- # 這裡會自動把帶有「括號」或廠商名稱的學名藥濾掉
324
  if not is_generic(title, "", ing_ja):
325
  brands.add(clean_brand_name(title))
326
  companies.add(tds[2].get_text(separator=" ", strip=True).replace('製造販売元/', ''))
327
- else:
328
- log.append(f"❌ 第 {current_page} 頁未出現 ResultList。")
329
- break
330
 
331
- # 尋找下一頁的按鈕 (例如 javascript:changePg(2);)
332
  current_page += 1
333
  next_page_link = popup.locator(f'a[href="javascript:changePg({current_page});"]')
334
-
335
  if next_page_link.count() > 0:
336
- log.append(f" -> 發現第 {current_page} 頁,執行翻頁...")
337
  next_page_link.first.click(force=True)
338
- popup.wait_for_timeout(2000) # 給予表格重新渲染的緩衝時間
339
- else:
340
- log.append(" -> 已無下一頁,結束掃描。")
341
- break
342
 
343
  popup.close()
344
-
345
  if brands: return ", ".join(brands), ", ".join(companies), "\n".join(log)
346
- return "查無原廠", "-", "\n".join(log) + "\n❌ 查無資料或全為學名藥"
347
- except Exception as e:
348
- return "執行失敗", "-", "\n".join(log) + f"\n錯誤: {str(e)}"
349
 
350
  def get_switzerland_originator(ing_de, page):
351
  log, brands, companies = [], set(), set()
352
  try:
353
  log.append(f"1. 前往 Swissmedicinfo Pro 專業版 (搜尋: {ing_de})...")
354
  page.goto("https://swissmedicinfo-pro.ch/?Lang=EN", timeout=45000, wait_until="domcontentloaded")
355
-
356
- log.append("2. 精準定位「成分專用」搜尋框 (txtSubstance)...")
357
- # 💡 直接使用您提供的精準 ID,這招絕對萬無一失!
358
  search_input = page.locator('input#MainContent_ucSearch1_txtSubstance')
359
  search_input.wait_for(state="attached", timeout=15000)
360
  search_input.fill(ing_de, force=True)
361
-
362
- log.append("3. 按下 Enter 並等待 ASP.NET 載入完整資料...")
363
  search_input.press("Enter")
364
 
365
  page.wait_for_load_state('networkidle', timeout=15000)
366
  page.wait_for_timeout(3000)
367
-
368
  page.wait_for_selector('table[id*="GVMonographies"], #MainContent_LabelNoResult', timeout=15000)
369
 
370
  soup = BeautifulSoup(page.content(), 'html.parser')
@@ -372,37 +442,27 @@ def get_switzerland_originator(ing_de, page):
372
 
373
  if table and table.find('tbody'):
374
  rows = table.find('tbody').find_all('tr', class_=re.compile(r'clickable-row'))
375
- log.append(f"4. 畫面解析完成,找到 {len(rows)} 筆表格資料。")
376
-
377
  for tr in rows:
378
  tds = tr.find_all('td')
379
  if len(tds) >= 4:
380
  title = tds[0].get_text(strip=True)
381
  comp = tds[3].get_text(strip=True)
382
-
383
  if not is_generic(title, comp, ing_de):
384
  brands.add(clean_brand_name(title))
385
  if comp != "-": companies.add(comp)
386
- else:
387
- log.append("❌ 畫面上未出現表格,查無資料。")
388
-
389
  if brands: return ", ".join(brands), ", ".join(companies), "\n".join(log)
390
- return "查無原廠", "-", "\n".join(log) + "\n❌ 查無資料或全為學名藥"
391
- except Exception as e:
392
- return "執行失敗", "-", "\n".join(log) + f"\n錯誤: {str(e)}"
393
 
394
  def get_germany_originator(ing_de, page):
395
  log, brands, companies = [], set(), set()
396
  try:
397
- log.append(f"1. 前往 Gelbe Liste (搜尋: {ing_de})...")
398
  page.goto(f"https://www.gelbe-liste.de/profi-suche/results?substance={quote(ing_de)}", timeout=30000)
399
  page.wait_for_selector('.product-list', timeout=15000)
400
-
401
  soup = BeautifulSoup(page.content(), 'html.parser')
402
  ul = soup.find('ul', class_='product-list')
403
  if ul:
404
  lis = ul.find_all('li')
405
- log.append(f"2. 找到 {len(lis)} 筆結果,過濾平行輸入商。")
406
  for li in lis:
407
  h5, p_tag = li.find('h5'), li.find('p', class_='small')
408
  if h5:
@@ -411,141 +471,25 @@ def get_germany_originator(ing_de, page):
411
  brands.add(clean_brand_name(title))
412
  companies.add(comp)
413
  if brands: return ", ".join(brands), ", ".join(companies), "\n".join(log)
414
- return "查無原廠", "-", "\n".join(log) + "\n❌ 查無資料"
415
  except Exception as e: return "執行失敗", "-", "\n".join(log) + f"\n錯誤: {str(e)}"
416
 
417
- def get_france_originator(ingredient, page):
418
- log, brands, companies = [], set(), set()
419
- try:
420
- log.append(f"1. 前往 Vidal 搜尋頁面 (搜尋: {ingredient})...")
421
- page.goto(f"https://www.vidal.fr/recherche.html?query={quote(ingredient)}", timeout=45000, wait_until="domcontentloaded")
422
-
423
- log.append("2. 等待搜尋結果渲染...")
424
- page.wait_for_selector('.results, .searchbar', timeout=15000)
425
-
426
- soup = BeautifulSoup(page.content(), 'html.parser')
427
- divs = soup.find_all('div', class_=re.compile(r'result drug'))
428
- log.append(f"3. 找到 {len(divs)} 筆 'result drug' 區塊。")
429
-
430
- cands = []
431
- for div in divs:
432
- info_div = div.find('div', class_='infos')
433
- if info_div and info_div.find('a'):
434
- a_tag = info_div.find('a')
435
- title = a_tag.get_text(strip=True)
436
- href = a_tag.get('href', '')
437
-
438
- # 第一階段:先用商品名初步過濾掉明顯的學名藥
439
- if not is_generic(title, "", ingredient):
440
- cands.append({"raw_title": title, "href": href})
441
-
442
- if cands:
443
- log.append(f"4. 篩選出 {len(cands)} 筆非學名藥候選,準備進入內頁抓取藥廠 (Laboratoire)...")
444
-
445
- for cand in cands:
446
- title = cand['raw_title']
447
- href = cand['href']
448
-
449
- # 組裝絕對網址
450
- if href.startswith('http'):
451
- detail_url = href
452
- elif href.startswith('/'):
453
- detail_url = f"https://www.vidal.fr{href}"
454
- else:
455
- detail_url = f"https://www.vidal.fr/{href}"
456
-
457
- log.append(f" -> 進入內頁: {clean_brand_name(title)}")
458
- page.goto(detail_url, timeout=30000, wait_until="domcontentloaded")
459
-
460
- comp = "-"
461
- try:
462
- # 💡 依照您提供的 HTML 結構,等待並精準抓取 div.nomlab
463
- page.wait_for_selector('div.nomlab', timeout=10000)
464
- detail_soup = BeautifulSoup(page.content(), 'html.parser')
465
- nomlab_div = detail_soup.find('div', class_='nomlab')
466
-
467
- if nomlab_div:
468
- comp = nomlab_div.get_text(strip=True)
469
- log.append(f" ✅ 成功取得藥廠: {comp}")
470
- except Exception:
471
- log.append(" ❌ 取得藥廠失敗或 Timeout。")
472
-
473
- # 第二階段:取得藥廠後,進行最終嚴格過濾 (確保萬無一失)
474
- if not is_generic(title, comp, ingredient):
475
- brands.add(clean_brand_name(title))
476
- if comp != "-": companies.add(comp)
477
-
478
- if brands:
479
- return ", ".join(brands), ", ".join(companies), "\n".join(log)
480
-
481
- return "查無原廠", "-", "\n".join(log) + "\n❌ 查無非學名藥資料"
482
- except Exception as e:
483
- return "執行失敗", "-", "\n".join(log) + f"\n錯誤: {str(e)}"
484
-
485
- def get_sweden_originator(ingredient, page):
486
- log, brands, companies = [], set(), set()
487
- try:
488
- log.append(f"1. 前往瑞典 FASS 搜尋頁面 (搜尋: {ingredient})...")
489
- page.goto(f"https://fass.se/search?query={quote(ingredient)}", timeout=45000, wait_until="domcontentloaded")
490
-
491
- log.append("2. 鎖定特定的搜尋結果區塊...")
492
- try:
493
- page.wait_for_selector('details.app-toggle-details-icon, .no-results', state="attached", timeout=15000)
494
- except Exception:
495
- log.append(" -> 查無 details 節點,判斷為無結果。")
496
-
497
- page.wait_for_timeout(2000) # 給予前端框架充裕時間建構 DOM 樹
498
-
499
- soup = BeautifulSoup(page.content(), 'html.parser')
500
- items = soup.find_all('details', class_=re.compile(r'app-toggle-details-icon'))
501
- log.append(f"3. 找到 {len(items)} 筆搜尋結果。")
502
-
503
- for item in items:
504
- # 1. 從 summary 標籤中抓取商品名
505
- summary = item.find('summary')
506
- if not summary: continue
507
-
508
- title_span = summary.find('span', class_=re.compile(r'font-semibold'))
509
- title = title_span.get_text(strip=True) if title_span else ""
510
-
511
- # 2. 💡 修正:深入內層的 <ol> 列表,精準抓取真正的藥廠名稱
512
- comp = "-"
513
- ol = item.find('ol')
514
- if ol:
515
- # 這樣就不會抓到外層的 Bilastin 了
516
- comp_span = ol.find('span', class_='text-label-md')
517
- if comp_span:
518
- comp = comp_span.get_text(strip=True)
519
-
520
- # 3. 執行黑名單過濾 (此時 comp 會是 Tiefenbacher 等,過濾器將成功攔截)
521
- if title:
522
- if not is_generic(title, comp, ingredient):
523
- brands.add(clean_brand_name(title))
524
- if comp != "-": companies.add(comp)
525
-
526
- if brands: return ", ".join(brands), ", ".join(companies), "\n".join(log)
527
- return "查無原廠", "-", "\n".join(log) + "\n❌ 查無資料或全為學名藥"
528
- except Exception as e:
529
- return "執行失敗", "-", "\n".join(log) + f"\n錯誤: {str(e)}"
530
-
531
  # ==========================================
532
  # 🚀 主執行中樞
533
  # ==========================================
534
  def run_all_ten_countries(ing_en, ing_ja_manual, ing_de_manual):
535
  if not ing_en: return [["錯誤", "請輸入英文成分名", "-", ""]]
536
 
 
537
  ing_ja = ing_ja_manual if ing_ja_manual else translate_lang(ing_en, 'ja')
538
  ing_de = ing_de_manual if ing_de_manual else translate_lang(ing_en, 'de')
539
  results = []
540
 
541
- # 1. API 模組 (不受瀏覽器阻擋)
542
- usa_b, usa_c, usa_log = get_usa_originator(ing_en)
543
- results.append(["🇺🇸 美國 (FDA)", usa_b, usa_c, usa_log])
544
-
545
  au_b, au_c, au_log = get_australia_originator(ing_en)
546
  results.append(["🇦🇺 澳洲 (TGA)", au_b, au_c, au_log])
547
 
548
- # 2. 瀏覽器模組 (Playwright 嚴格隔離)
549
  with sync_playwright() as p:
550
  browser = p.chromium.launch(headless=True, args=['--no-sandbox', '--disable-dev-shm-usage'])
551
  context = browser.new_context(user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/122.0.0.0 Safari/537.36")
@@ -556,14 +500,15 @@ def run_all_ten_countries(ing_en, ing_ja_manual, ing_de_manual):
556
  page.close()
557
  results.append([name, b, c, log])
558
 
559
- run_pw(get_france_originator, ing_en, "🇫🇷 國 (Vidal)") # 新增這行到 Playwright 區塊
560
  run_pw(get_belgium_originator, ing_en, "🇧🇪 比利時 (CBIP)")
 
561
  run_pw(get_uk_originator, ing_en, "🇬🇧 英國 (eMC)")
562
  run_pw(get_canada_originator, ing_en, "🇨🇦 加拿大 (DPD)")
563
  run_pw(get_japan_originator, ing_ja, "🇯🇵 日本 (PMDA)")
564
  run_pw(get_switzerland_originator, ing_de, "🇨🇭 瑞士 (Swissmedicinfo)")
565
- run_pw(get_germany_originator, ing_de, "🇩🇪 德國 (Gelbe Liste)")
566
  run_pw(get_sweden_originator, ing_en, "🇸🇪 瑞典 (FASS)")
 
567
 
568
  browser.close()
569
 
@@ -573,13 +518,13 @@ def run_all_ten_countries(ing_en, ing_ja_manual, ing_de_manual):
573
  # 🎨 UI 介面
574
  # ==========================================
575
  with gr.Blocks(title="十國原廠商品名智能檢索器") as demo:
576
- gr.Markdown("## 🌐 跨國原廠商品名檢索器 (高階診斷版)")
577
 
578
  with gr.Row():
579
  ing_en = gr.Textbox(label="🧪 英文成分名 (必填)", placeholder="例如: Semaglutide")
580
  with gr.Row():
581
  with gr.Accordion("⚙️ 手動覆寫翻譯 (進階)", open=False):
582
- ing_ja = gr.Textbox(label="🇯🇵 日文成分名", placeholder="若空白則自動翻譯")
583
  ing_de = gr.Textbox(label="🇩🇪 德文成分名", placeholder="若空白則自動翻譯")
584
 
585
  search_btn = gr.Button("🚀 啟動十國查詢", variant="primary")
 
8
  from curl_cffi import requests as curl_req
9
  import gradio as gr
10
 
11
+ # 💡 匯入 Hugging Face 原生 AI 翻譯所需套件
12
+ import torch
13
+ from transformers import pipeline
14
+
15
  os.system("playwright install chromium")
16
 
17
+ # ==========================================
18
+ # 🧠 載入原生 ElanMT 醫療翻譯模型 (依據您提供的程式碼)
19
+ # ==========================================
20
+ print("⏳ 系統啟動中:正在載入 ElanMT 醫療翻譯模型...")
21
+ device = "cuda" if torch.cuda.is_available() else "cpu"
22
+ try:
23
+ # 根據您的 app.py,直接呼叫 transformers pipeline
24
+ translator_en_ja = pipeline("translation", model="Mitsua/elan-mt-bt-en-ja", device=device)
25
+ print(f"✅ ElanMT 模型載入完成!(執行環境: {device})")
26
+ except Exception as e:
27
+ print(f"⚠️ ElanMT 模型載入失敗,將退回備用機制。錯誤: {e}")
28
+ translator_en_ja = None
29
+
30
+
31
+ # ==========================================
32
+ # 🛠️ 共用工具:四重智能翻譯與學名藥濾網
33
+ # ==========================================
34
  def get_official_japanese_name(ingredient_en):
35
+ """利用維基百科 API 跨語言連結,精準取得官方日文藥名 (JAN)"""
 
 
36
  try:
 
37
  search_url = f"https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch={quote(ingredient_en)}&utf8=&format=json"
38
  res = requests.get(search_url, timeout=5).json()
39
  if not res['query']['search']: return None
40
 
 
41
  en_title = res['query']['search'][0]['title']
 
 
42
  lang_url = f"https://en.wikipedia.org/w/api.php?action=query&titles={quote(en_title)}&prop=langlinks&lllang=ja&format=json"
43
  lang_res = requests.get(lang_url, timeout=5).json()
44
 
45
  pages = lang_res['query']['pages']
46
  for page_id in pages:
47
  if 'langlinks' in pages[page_id]:
 
48
  ja_title = pages[page_id]['langlinks'][0]['*']
 
49
  return ja_title.split(' ')[0].split('(')[0]
50
  except Exception:
51
  pass
52
  return None
53
 
54
  def translate_lang(text, target_lang):
55
+ """四重防護智能翻譯分發器"""
56
+ text_lower = text.lower().strip()
57
+
 
58
  if target_lang == 'ja':
59
+ # 1. 字典防護
60
+ ja_overrides = {
61
+ "bilastine": "ビラスチン",
62
+ "semaglutide": "セマグルチド"
63
+ }
64
+ if text_lower in ja_overrides:
65
+ print(f"🌍 [翻譯] 命中自訂字典: {text} -> {ja_overrides[text_lower]}")
66
+ return ja_overrides[text_lower]
67
+
68
+ # 2. 💡 原生 ElanMT 模型防護 (不需對外連線)
69
+ if translator_en_ja is not None:
70
+ try:
71
+ # 呼叫 pipeline 進行推論
72
+ result = translator_en_ja(text)
73
+ translated_text = result[0]['translation_text']
74
+ print(f"🌍 [翻譯] 命中 ElanMT 醫療模型: {text} -> {translated_text}")
75
+ return translated_text
76
+ except Exception as e:
77
+ print(f"⚠️ ElanMT 推論錯誤 ({e}),降級至維基百科...")
78
+
79
+ # 3. 維基百科防護
80
  official_ja = get_official_japanese_name(text)
81
  if official_ja:
82
+ print(f"🌍 [翻譯] 命中維基百科: {text} -> {official_ja}")
83
+ return official_ja
84
 
85
+ # 4. Google 翻譯兜底 (德文或前述皆失敗時)
86
  try:
87
  url = f"https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl={target_lang}&dt=t&q={quote(text)}"
88
  res = requests.get(url, timeout=5)
89
  if res.status_code == 200:
90
+ result = res.json()[0][0][0].strip()
91
+ print(f"🌍 [翻譯] 命中 Google 翻譯: {text} -> {result}")
92
+ return result
93
  except Exception:
94
  pass
 
95
  return text
96
 
97
  def is_generic(brand_name, company_name, ingredient):
 
99
  c_lower = company_name.lower()
100
  i_lower = ingredient.lower()
101
 
 
102
  generic_keywords = [
103
  'sandoz', 'teva', 'apotex', 'ratiopharm', 'jamp', 'mint', 'pharmascience', 'sanis', 'sivem',
104
  'auro', 'glenmark', 'taro', 'marcan', 'nora', 'mantra', 'reddy', 'mepha', 'axapharm',
 
116
  return False
117
 
118
  def clean_brand_name(raw_name):
119
+ # 去劑型化:精準擷取商品名主體
 
 
 
 
 
 
120
  pattern = r'(皮下注|錠|カプセル|顆粒|シロップ|OD|細粒|液|Augentropfen|Schmelztabletten|Tabletten|kids|Lingual|Sol|cp|inj|pen|prefilled|flex|\d+)'
 
 
121
  cleaned = re.split(pattern, raw_name, flags=re.IGNORECASE)[0]
 
 
122
  return cleaned.replace('®', '').replace('™', '').strip(' -_')
123
 
124
+
125
  # ==========================================
126
+ # 🚀 模組 A:使用 curl_cffi 抓取 (澳)
127
  # ==========================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  def get_australia_originator(ingredient):
129
  log, brands = [], set()
130
  try:
131
  session = curl_req.Session(impersonate="chrome120")
132
  log.append("1. 發送 GET 至澳洲 TGA 搜尋頁面...")
133
  res = session.get(f"https://www.tga.gov.au/resources/artg?keywords={ingredient}", timeout=30, verify=False)
 
134
  soup = BeautifulSoup(res.text, 'html.parser')
135
 
136
  articles = soup.find_all('article', class_='node--artg')
 
144
  if len(parts) > 1 and parts[0].strip():
145
  brand = parts[0].strip()
146
  if not is_generic(brand, "", ingredient):
 
147
  a_tag = title_tag.find('a')
148
  href = a_tag['href'] if a_tag else None
149
  cands.append({"brand": brand, "date": time_tag.get('datetime'), "href": href})
150
 
151
  if cands:
 
152
  cands = sorted(cands, key=lambda x: x['date'])
153
  target = cands[0]
154
  brand = target['brand']
155
  log.append(f"✅ 依日期排序鎖定最早註冊: {target['date'][:10]} ({brand})")
156
 
157
+ company = "TGA資料庫"
 
 
158
  if target['href']:
 
159
  detail_url = f"https://www.tga.gov.au{target['href']}" if target['href'].startswith('/') else target['href']
160
  log.append("3. 進入內頁抓取 Sponsor 藥廠資訊...")
 
161
  res_detail = session.get(detail_url, timeout=30, verify=False)
162
  if res_detail.status_code == 200:
163
  detail_soup = BeautifulSoup(res_detail.text, 'html.parser')
 
 
164
  sponsor_div = detail_soup.find('div', class_=re.compile(r'field--name-field-sponsor'))
165
  if sponsor_div and sponsor_div.find('a'):
166
  company = sponsor_div.find('a').get_text(strip=True)
167
  log.append(f" -> 成功取得藥廠名稱: {company}")
 
 
 
 
 
168
  return brand, company, "\n".join(log)
 
169
  return "查無原廠", "-", "\n".join(log)
170
  except Exception as e:
171
  return "執行失敗", "-", "\n".join(log) + f"\n錯誤: {str(e)}"
172
 
173
+
174
  # ==========================================
175
+ # 🚀 模組 B:使用 Playwright 抓取 (美、比、法、英、加、日、德、瑞、瑞典)
176
  # ==========================================
177
+ def get_usa_originator(ingredient, page):
178
+ log, brands, companies = [], set(), set()
179
+ try:
180
+ log.append(f"1. 前往 FDA Orange Book (搜尋: {ingredient})...")
181
+ url = f"https://www.accessdata.fda.gov/scripts/cder/ob/results_product.cfm?Generic_Name={quote(ingredient)}&rx_otc=All"
182
+ page.goto(url, timeout=45000, wait_until="domcontentloaded")
183
+
184
+ log.append("2. 等待 DataTables (table#example) 動態渲染...")
185
+ try:
186
+ page.wait_for_selector('table#example, .alert-warning', state="attached", timeout=15000)
187
+ except Exception:
188
+ pass
189
+
190
+ page.wait_for_timeout(2000)
191
+ soup = BeautifulSoup(page.content(), 'html.parser')
192
+ table = soup.find('table', id='example')
193
+
194
+ if table and table.find('tbody'):
195
+ rows = table.find('tbody').find_all('tr')
196
+ log.append(f"3. 找到表格,共 {len(rows)} 列資料。")
197
+ headers = [th.get_text(strip=True).lower() for th in table.find_all('th')]
198
+ brand_idx = next((i for i, h in enumerate(headers) if 'proprietary name' in h), 2)
199
+ rld_idx = next((i for i, h in enumerate(headers) if 'rld' in h), 8)
200
+ mfg_idx = next((i for i, h in enumerate(headers) if 'applicant holder' in h), 10)
201
+
202
+ for tr in rows:
203
+ tds = tr.find_all('td')
204
+ if len(tds) > max(rld_idx, brand_idx):
205
+ rld_text = tds[rld_idx].get_text(strip=True).upper()
206
+ if "RLD" in rld_text or "RS" in rld_text:
207
+ title = tds[brand_idx].get_text(strip=True)
208
+ comp = tds[mfg_idx].get_text(strip=True) if len(tds) > mfg_idx else "-"
209
+
210
+ if not is_generic(title, comp, ingredient):
211
+ brands.add(clean_brand_name(title))
212
+ if comp != "-": companies.add(comp)
213
+ else:
214
+ log.append("❌ 畫面上未出現 table#example,可能查無此藥或遭阻擋。")
215
+
216
+ if brands: return ", ".join(brands), ", ".join(companies), "\n".join(log)
217
+ return "查無原廠", "-", "\n".join(log) + "\n❌ 查無資料或全為學名藥"
218
+ except Exception as e:
219
+ return "執行失敗", "-", "\n".join(log) + f"\n錯誤: {str(e)}"
220
+
221
+ def get_france_originator(ingredient, page):
222
+ log, brands, companies = [], set(), set()
223
+ try:
224
+ log.append(f"1. 前往 Vidal 搜尋頁面 (搜尋: {ingredient})...")
225
+ page.goto(f"https://www.vidal.fr/recherche.html?query={quote(ingredient)}", timeout=45000, wait_until="domcontentloaded")
226
+ page.wait_for_selector('.results, .searchbar', timeout=15000)
227
+
228
+ soup = BeautifulSoup(page.content(), 'html.parser')
229
+ divs = soup.find_all('div', class_=re.compile(r'result drug'))
230
+ log.append(f"3. 找到 {len(divs)} 筆 'result drug' 區塊。")
231
+
232
+ cands = []
233
+ for div in divs:
234
+ info_div = div.find('div', class_='infos')
235
+ if info_div and info_div.find('a'):
236
+ a_tag = info_div.find('a')
237
+ title = a_tag.get_text(strip=True)
238
+ href = a_tag.get('href', '')
239
+ if not is_generic(title, "", ingredient):
240
+ cands.append({"raw_title": title, "href": href})
241
+
242
+ if cands:
243
+ log.append(f"4. 篩選出 {len(cands)} 筆非學名藥候選,準備進入內頁抓取藥廠...")
244
+ for cand in cands:
245
+ title = cand['raw_title']
246
+ href = cand['href']
247
+ detail_url = href if href.startswith('http') else (f"https://www.vidal.fr{href}" if href.startswith('/') else f"https://www.vidal.fr/{href}")
248
+
249
+ log.append(f" -> 進入內頁: {clean_brand_name(title)}")
250
+ page.goto(detail_url, timeout=30000, wait_until="domcontentloaded")
251
+
252
+ comp = "-"
253
+ try:
254
+ page.wait_for_selector('div.nomlab', timeout=10000)
255
+ detail_soup = BeautifulSoup(page.content(), 'html.parser')
256
+ nomlab_div = detail_soup.find('div', class_='nomlab')
257
+ if nomlab_div:
258
+ comp = nomlab_div.get_text(strip=True)
259
+ log.append(f" ✅ 成功取得藥廠: {comp}")
260
+ except Exception:
261
+ pass
262
+
263
+ if not is_generic(title, comp, ingredient):
264
+ brands.add(clean_brand_name(title))
265
+ if comp != "-": companies.add(comp)
266
+
267
+ if brands: return ", ".join(brands), ", ".join(companies), "\n".join(log)
268
+ return "查無原廠", "-", "\n".join(log) + "\n❌ 查無非學名藥資料"
269
+ except Exception as e:
270
+ return "執行失敗", "-", "\n".join(log) + f"\n錯誤: {str(e)}"
271
+
272
+ def get_sweden_originator(ingredient, page):
273
+ log, brands, companies = [], set(), set()
274
+ try:
275
+ log.append(f"1. 前往瑞典 FASS 搜尋頁面 (搜尋: {ingredient})...")
276
+ page.goto(f"https://fass.se/search?query={quote(ingredient)}", timeout=45000, wait_until="domcontentloaded")
277
+
278
+ try:
279
+ page.wait_for_selector('details.app-toggle-details-icon, .no-results', state="attached", timeout=15000)
280
+ except Exception:
281
+ pass
282
+
283
+ page.wait_for_timeout(2000)
284
+ soup = BeautifulSoup(page.content(), 'html.parser')
285
+ items = soup.find_all('details', class_=re.compile(r'app-toggle-details-icon'))
286
+ log.append(f"3. 找到 {len(items)} 筆搜尋結果。")
287
+
288
+ for item in items:
289
+ summary = item.find('summary')
290
+ if not summary: continue
291
+
292
+ title_span = summary.find('span', class_=re.compile(r'font-semibold'))
293
+ title = title_span.get_text(strip=True) if title_span else ""
294
+
295
+ comp = "-"
296
+ ol = item.find('ol')
297
+ if ol:
298
+ comp_span = ol.find('span', class_='text-label-md')
299
+ if comp_span:
300
+ comp = comp_span.get_text(strip=True)
301
+
302
+ if title:
303
+ if not is_generic(title, comp, ingredient):
304
+ brands.add(clean_brand_name(title))
305
+ if comp != "-": companies.add(comp)
306
+
307
+ if brands: return ", ".join(brands), ", ".join(companies), "\n".join(log)
308
+ return "查無原廠", "-", "\n".join(log) + "\n❌ 查無資料或全為學名藥"
309
+ except Exception as e:
310
+ return "執行失敗", "-", "\n".join(log) + f"\n錯誤: {str(e)}"
311
+
312
  def get_belgium_originator(ingredient, page):
313
  log, brands = [], set()
314
  try:
315
  log.append(f"1. 前往 CBIP (比利時) 搜尋頁面 (搜尋: {ingredient})...")
316
  page.goto(f"https://www.cbip.be/fr/search?q={ingredient}", timeout=45000, wait_until="domcontentloaded")
 
 
317
  page.wait_for_selector('ul.search-results, .no-results', timeout=15000)
318
  page.wait_for_timeout(2000)
319
 
320
  soup = BeautifulSoup(page.content(), 'html.parser')
 
 
 
321
  results_list = soup.find('ul', class_=re.compile(r'search-results|results'))
322
 
323
  if results_list:
324
  links = results_list.find_all('a')
325
  for link in links:
326
  text = link.get_text(strip=True)
 
 
327
  if len(text) > 3 and not text.lower().startswith('voir'):
 
328
  if not is_generic(text, "", ingredient):
329
  brands.add(clean_brand_name(text))
 
 
 
330
  if brands: return ", ".join(brands), "CBIP 資料庫", "\n".join(log)
331
  return "查無原廠", "-", "\n".join(log) + "\n❌ 查無資料或全為學名藥"
332
  except Exception as e:
 
340
  page.wait_for_selector('.search-results-product-info-title-link', timeout=15000)
341
  soup = BeautifulSoup(page.content(), 'html.parser')
342
  links = soup.find_all('a', class_='search-results-product-info-title-link')
 
343
  for link in links:
344
  title = link.get_text(strip=True)
345
  if not title.lower().startswith(ingredient.lower()):
 
358
  page.goto("https://health-products.canada.ca/dpd-bdpp/index-eng.jsp", timeout=45000, wait_until="domcontentloaded")
359
  page.locator('input[id="activeIngredient"]').fill(ingredient)
360
  page.keyboard.press("Enter")
 
 
361
  page.wait_for_selector('table#results, .alert-info, .alert-warning', timeout=15000)
362
  soup = BeautifulSoup(page.content(), 'html.parser')
363
  table = soup.find('table', id='results')
364
 
365
+ if not table or not table.find('tbody'): return "查無資料", "-", "\n".join(log)
 
 
 
366
  rows = table.find('tbody').find_all('tr')
 
367
  all_cands = []
368
  for tr in rows:
369
  tds = tr.find_all('td')
 
375
  all_cands.append({"company": comp, "product": tds[3].get_text(strip=True), "din": int(m.group())})
376
 
377
  if not all_cands: return "查無原廠", "-", "\n".join(log) + "\n❌ 剩餘皆為學名藥廠"
 
378
  all_cands = sorted(all_cands, key=lambda x: x['din'])
379
  orig_comp = all_cands[0]['company']
380
  brands = set([c['product'] for c in all_cands if c['company'] == orig_comp])
381
+ return ", ".join(clean_brand_name(b) for b in brands), orig_comp, "\n".join(log)
 
382
  except Exception as e: return "執行失敗", "-", "\n".join(log) + f"\n錯誤: {str(e)}"
383
 
384
  def get_japan_originator(ing_ja, page):
 
386
  try:
387
  log.append(f"1. 前往 PMDA (搜尋: {ing_ja})...")
388
  page.goto("https://www.pmda.go.jp/PmdaSearch/iyakuSearch/", timeout=45000, wait_until="domcontentloaded")
 
 
 
389
  search_input = page.locator('input#txtName')
390
  search_input.wait_for(state="attached", timeout=15000)
391
  search_input.fill(ing_ja, force=True)
392
 
 
393
  with page.expect_popup() as popup_info:
394
  search_input.press("Enter")
395
  popup = popup_info.value
 
 
396
  current_page = 1
397
 
398
  while True:
 
399
  popup.wait_for_selector('table#ResultList, .errormsg, .non-result', timeout=15000)
400
  soup = BeautifulSoup(popup.content(), 'html.parser')
401
  table = soup.find('table', id='ResultList')
402
 
403
  if table:
404
  rows = table.find_all('tr')
 
405
  for tr in rows:
406
  tds = tr.find_all('td')
407
  if len(tds) >= 3:
408
  title = tds[1].get_text(strip=True)
 
409
  if not is_generic(title, "", ing_ja):
410
  brands.add(clean_brand_name(title))
411
  companies.add(tds[2].get_text(separator=" ", strip=True).replace('製造販売元/', ''))
412
+ else: break
 
 
413
 
 
414
  current_page += 1
415
  next_page_link = popup.locator(f'a[href="javascript:changePg({current_page});"]')
 
416
  if next_page_link.count() > 0:
 
417
  next_page_link.first.click(force=True)
418
+ popup.wait_for_timeout(2000)
419
+ else: break
 
 
420
 
421
  popup.close()
 
422
  if brands: return ", ".join(brands), ", ".join(companies), "\n".join(log)
423
+ return "查無原廠", "-", "\n".join(log)
424
+ except Exception as e: return "執行失敗", "-", "\n".join(log) + f"\n錯誤: {str(e)}"
 
425
 
426
  def get_switzerland_originator(ing_de, page):
427
  log, brands, companies = [], set(), set()
428
  try:
429
  log.append(f"1. 前往 Swissmedicinfo Pro 專業版 (搜尋: {ing_de})...")
430
  page.goto("https://swissmedicinfo-pro.ch/?Lang=EN", timeout=45000, wait_until="domcontentloaded")
 
 
 
431
  search_input = page.locator('input#MainContent_ucSearch1_txtSubstance')
432
  search_input.wait_for(state="attached", timeout=15000)
433
  search_input.fill(ing_de, force=True)
 
 
434
  search_input.press("Enter")
435
 
436
  page.wait_for_load_state('networkidle', timeout=15000)
437
  page.wait_for_timeout(3000)
 
438
  page.wait_for_selector('table[id*="GVMonographies"], #MainContent_LabelNoResult', timeout=15000)
439
 
440
  soup = BeautifulSoup(page.content(), 'html.parser')
 
442
 
443
  if table and table.find('tbody'):
444
  rows = table.find('tbody').find_all('tr', class_=re.compile(r'clickable-row'))
 
 
445
  for tr in rows:
446
  tds = tr.find_all('td')
447
  if len(tds) >= 4:
448
  title = tds[0].get_text(strip=True)
449
  comp = tds[3].get_text(strip=True)
 
450
  if not is_generic(title, comp, ing_de):
451
  brands.add(clean_brand_name(title))
452
  if comp != "-": companies.add(comp)
 
 
 
453
  if brands: return ", ".join(brands), ", ".join(companies), "\n".join(log)
454
+ return "查無原廠", "-", "\n".join(log)
455
+ except Exception as e: return "執行失敗", "-", "\n".join(log) + f"\n錯誤: {str(e)}"
 
456
 
457
  def get_germany_originator(ing_de, page):
458
  log, brands, companies = [], set(), set()
459
  try:
 
460
  page.goto(f"https://www.gelbe-liste.de/profi-suche/results?substance={quote(ing_de)}", timeout=30000)
461
  page.wait_for_selector('.product-list', timeout=15000)
 
462
  soup = BeautifulSoup(page.content(), 'html.parser')
463
  ul = soup.find('ul', class_='product-list')
464
  if ul:
465
  lis = ul.find_all('li')
 
466
  for li in lis:
467
  h5, p_tag = li.find('h5'), li.find('p', class_='small')
468
  if h5:
 
471
  brands.add(clean_brand_name(title))
472
  companies.add(comp)
473
  if brands: return ", ".join(brands), ", ".join(companies), "\n".join(log)
474
+ return "查無原廠", "-", "\n".join(log)
475
  except Exception as e: return "執行失敗", "-", "\n".join(log) + f"\n錯誤: {str(e)}"
476
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
  # ==========================================
478
  # 🚀 主執行中樞
479
  # ==========================================
480
  def run_all_ten_countries(ing_en, ing_ja_manual, ing_de_manual):
481
  if not ing_en: return [["錯誤", "請輸入英文成分名", "-", ""]]
482
 
483
+ # 呼叫四重智能翻譯 (現在會自動優先使用您的 ElanMT 模型)
484
  ing_ja = ing_ja_manual if ing_ja_manual else translate_lang(ing_en, 'ja')
485
  ing_de = ing_de_manual if ing_de_manual else translate_lang(ing_en, 'de')
486
  results = []
487
 
488
+ # API 模組 (澳洲)
 
 
 
489
  au_b, au_c, au_log = get_australia_originator(ing_en)
490
  results.append(["🇦🇺 澳洲 (TGA)", au_b, au_c, au_log])
491
 
492
+ # 瀏覽器模組 (Playwright 嚴格隔離)
493
  with sync_playwright() as p:
494
  browser = p.chromium.launch(headless=True, args=['--no-sandbox', '--disable-dev-shm-usage'])
495
  context = browser.new_context(user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/122.0.0.0 Safari/537.36")
 
500
  page.close()
501
  results.append([name, b, c, log])
502
 
503
+ run_pw(get_usa_originator, ing_en, "🇺🇸 國 (FDA)")
504
  run_pw(get_belgium_originator, ing_en, "🇧🇪 比利時 (CBIP)")
505
+ run_pw(get_france_originator, ing_en, "🇫🇷 法國 (Vidal)")
506
  run_pw(get_uk_originator, ing_en, "🇬🇧 英國 (eMC)")
507
  run_pw(get_canada_originator, ing_en, "🇨🇦 加拿大 (DPD)")
508
  run_pw(get_japan_originator, ing_ja, "🇯🇵 日本 (PMDA)")
509
  run_pw(get_switzerland_originator, ing_de, "🇨🇭 瑞士 (Swissmedicinfo)")
 
510
  run_pw(get_sweden_originator, ing_en, "🇸🇪 瑞典 (FASS)")
511
+ run_pw(get_germany_originator, ing_de, "🇩🇪 德國 (Gelbe Liste)")
512
 
513
  browser.close()
514
 
 
518
  # 🎨 UI 介面
519
  # ==========================================
520
  with gr.Blocks(title="十國原廠商品名智能檢索器") as demo:
521
+ gr.Markdown("## 🌐 跨國原廠商品名檢索器 (搭載原生 ElanMT 醫療翻譯)")
522
 
523
  with gr.Row():
524
  ing_en = gr.Textbox(label="🧪 英文成分名 (必填)", placeholder="例如: Semaglutide")
525
  with gr.Row():
526
  with gr.Accordion("⚙️ 手動覆寫翻譯 (進階)", open=False):
527
+ ing_ja = gr.Textbox(label="🇯🇵 日文成分名", placeholder="若空白則自動啟動 ElanMT 翻譯")
528
  ing_de = gr.Textbox(label="🇩🇪 德文成分名", placeholder="若空白則自動翻譯")
529
 
530
  search_btn = gr.Button("🚀 啟動十國查詢", variant="primary")