2024年6月24日 星期一

Python: BeautifulSoup 與 querySelectorAll

2024年6月24日 星期一

上週接收了好心同事一整套的英文書 I Wonder Why (Kingfisher 2004版)。查了一下,新版的在書上有附語音檔的 QR code,跟小孩子說,沒關係,只要找到一個連結,應該就都能找出來了。

「將~將~」,真的找到了:

在這個連結的頁面中,除了該冊每兩頁有音檔以外,也有其它冊的連結,這樣子真的是一個連結全部搞定了。

音檔可以下載到載具中,這樣子就可以離線使用;但是要一個個手動按下載嗎?二十本書,應該會按到手廢掉了。

分析了一下網頁的原始碼,果然很結構化,這樣子要找出音檔的連結就方便多了。

剛好前幾天有示範 JavaScript 的 querySelector 跟 querySelectorAll 的應用給小孩子看,順便再複習一下,如何在頁面中找出網址。在瀏覽器開啟前述頁面,然後在開發人員工具中的 Console ,使用這樣的語法:

mp3 = '';
var links = document.querySelectorAll('.synopsis-wrapper li a');
for(var i=0; i<links.length; i++) {
  url = links[i].href;
  res = await fetch(url);
  html = await res.text();
  var parser = new DOMParser();
  doc = parser.parseFromString(html, 'text/html');
  doc.querySelectorAll('source:not([src*="_extras"])').forEach(s=> mp3+=s.src+'\n');
}
console.log(mp3);

最後會將所有書的 mp3 音檔連結,一行行合在一起呈現。

看來網站是有防跨域擷取的,所以使用瀏覽器的 fetch 抓網頁,只能在 I Wonder Why 的頁面中才能成功執行。

由音檔的網址來分析,其實主要分為兩種:

  • insides
  • extras

非「extras」的才是我要下載的。為什麼不說是「insides」的才是我要下載的?

哈!本來我是用:

querySelectorAll('source[src*="insides.mp3"]')

中括號的 Attribute selectors 限定 src 的內容帶有 insides 的才留著,結果有兩本書的內容就解析不到音檔的網址。人工查了一下,才發現一個是會在 .mp3 前多了 new,另一個則是連 insides 都沒有。所以最後使用了 not([src*="_extras"]) ,這樣一來,非「extras」的條件,簡化了語法,不然又要打很多字來設條件了。

擷取出來的音檔網址要怎麼下載檔案?

最方便的就是使用 wget,它有提供「-i」的參數,可以讀入存有網址清單的純文字檔,然後一個個的下載回來。

如果把這麼多檔案放在一個資料夾裡面,要使用實在不方便,所以決定用 Python 來試試。如果在 Google Colab 中執行,分析完音檔的網址,直接就儲存到 Google Drive 中,這樣子在行動載具,只要由 Google Drive 中開啟來用;或是乾脆整個下載到載具儲存更好。

在 Python 中想要使用類似 JavaScript DOMParser + querySelectorAll 的功能,第一個想到的就是 BeautifulSoup。BeautifulSoup 的 select 語法和 querySelectorAll 一樣,實在是太棒了。

底下將整個語法貼在下面:

import re
import os
import requests
import urllib.request
from bs4 import BeautifulSoup

rootFolder = 'drive/MyDrive/00-i-wonder-why'

menuPageUrl = 'https://audio.panmacmillan.com/i-wonder-why-snakes'
html = requests.get(menuPageUrl).text
soup = BeautifulSoup(html, 'html.parser')
links = soup.select('.synopsis-wrapper li a')

for a in links:
  url = a.get('href')
  html = requests.get(url).text
  soup = BeautifulSoup(html, 'html.parser')
  title = re.findall(r'i-wonder-why-(.*)$', url)[0]
  print(title)
  source = soup.select('source:not([src*="_extras.mp3"])')
  print(len(source))
  folder = f"{rootFolder}/{title}"
  if not os.path.exists(folder):
    os.makedirs(folder)
  list_filename = f'{folder}/00-list.txt'
  list_txt = ''
  for s in source:
    mp3URL = s.get('src')
    pp = re.findall(r"pp(\d+)-(\d+)[^\.]*\.mp3", mp3URL)
    filename = title
    for n in pp[0]:
      filename +=  '-' + n.rjust(2, '0')
    filename += '.mp3'
    list_txt += f'{filename}\r\n'
    #print(mp3URL)
    filename = f"{folder}/{filename}"
    urllib.request.urlretrieve(mp3URL, filename)
  if list_txt != '':
    f = open(list_filename, 'w')
    f.write(list_txt)
    f.close()

音檔下載後,將檔案名稱改得簡單一點,並儲存到各冊自己的資料夾中;也順便將音檔的清單存入各資料夾中的「00-list.txt」文字檔中。

「00-list.txt」是我為下一階段所做的準備。為了方便聽音檔時可以快速檢索,我使用 OpenAI Whisper ( 後來改用 Faster Whipser ) ,將音檔進行語音辨識,並輸出為字幕檔,因為一句句都帶有時間截記,要快速重聽某一句就方便多了。哈!我之前分享過的 OpenAI Whiper ,跟 Faster Whisper ,經過我的打造,都支援由文字檔中讀取音檔清單,然後也可以自行設定輸出的資料夾,輸入、輸出都設定到 Google Drive 我放 I Wonder Why 的音檔資料,這樣子就可以「一條龍」了!兩個工具可以參考之前的文章:

為什麼後來改用 Faster Whisper  ? 主要是 OpenAI Whisper 比較耗資源,解譯完一冊書的音檔,第二冊進行到一半,就被重置,這實在太令人氣結了。

不過,用著用著,才發現自己寫的程序有一些小臭蟲,前面的 「00-list.txt」中放的是只有音檔的檔案,因為不是絕對路徑,一直出現了找不到音檔的訊息,並沒有進行語音辨識;另外,輸出的資料夾如果沒有先建立好,也不會照設定儲存到雲端硬碟中,針對這些問題,會再更新到公開的版本中。

有了音檔,有了字幕檔,目前我們使用 oTranscribe 在 iPad 中聽語音 :

如果遇到需要查中文的,就直接將字詞選取以後,使用 iOS 內建的字典來看,整體看起來,還算可以。

字幕檔變雙語?試了一下,也滿好玩的,一行英文,一行中文,利用 Microsoft+Copilot ChatGPT 就能製作了,目前使用這樣的 prompt

translate the following subtitles text to traditional Chinese , treat as one continuous text, keep the source format and append Chinese to the next line of English :

ChatGPT 3.5

Translate the English subtitles text into traditional Chinese, add Chinese after the line of English. the subtitles is:

產生的雙語字幕檔,還需要利用 Regular Expression 處理一下,不過,一道指令就可以完成了。

還在構思是否要來寫個 HTML5 的閱讀小工具,可以比 oTranscribe 更方便的聽讀、查閱。

相關連結




沒有留言:

張貼留言

 
雄::gsyan © 2009. Design by Pocket