爬蟲 & Python

2024, May 09    

大綱

一、規格書
二、Python 環境建置
三、Python 爬蟲套件
四、程式邏輯
五、遇到的問題

一、規格書

  • 爬 3 個網站的「一覽頁」和「店鋪詳細頁」:
  • 第一階段:抓取一覽頁的「店鋪 url」和「所在地區」。
  • 第二階段:依「所在地區」排序,再依序撈取「店鋪詳細頁」的資訊。

二、Python 環境建置

Python 3.10 跟之前的版本有很大的差異, 例如:查詢 Python 版本的 CLI 不一樣。

  • Python 3.9

    python3 –version

  • Python 指令在 3.10 以上才有

    python -V

1. PyCharm 編輯器

JetBrains,phpStorm 開發商。

2. 安裝 Python

  • 在 mac 或 windows
  • 在 Linux
    • 安裝 pyenv (管理 Python 版本的工具),再用 pyenv 安裝 Python。

3. 安裝 pyenv 和 pip

使用 Python 開發時,會安裝 2 個必要工具,pyenv 和 pip,分別對應像是 node 的 nvm 和 npm。

  Python node
管理版本 pyenv nvm
管理套件 pip npm

安裝 pyenv

  • 管理 Python 版本的工具,像 node 的 nvm。
  • pyenv 安裝教學
  • 查看可以安裝的版本

    pyenv install -l

  • 安裝其中一種 Python 的版本

    pyenv install -v 3.12.2

  • 列出已安裝過的版本

    pyenv versions

  • 切換 Python 的版本,三種模式 local、global 或 shell。
    • 在當前目錄下設置 Python 版本,這只對當前目錄及其子目錄有效。

      pyenv local 3.12.2

    • 設置全局 Python 版本,所有 shell 都有效。

      pyenv global 3.12.2

    • 在當前 shell 中切換 Python 版本,當 shell 退出再進入,會跳回原本預設的版本。

      pyenv shell 3.12.2

創建 python 虛擬環境

  • 創建虛擬環境

    python -m venv ./.venv

  • 開通虛擬環境(Linux/macOS)

    source .venv/bin/activate

  • 退出虛擬環境

    deactivate

安裝 pip

  • 管理 Python 套件的工具,像 node 的 npm install 建立 package.json。
  • 安裝 pip

    sudo yum install python3-pip

  • 列出安裝過的套件和版本

    pip list -v

  • 安裝/解安裝套件 pip install [套件名稱],例如:

    pip install beautifulsoup4

  • 解安裝套件

    pip uninstall beautifulsoup4

  • 套件安裝資訊

    pip show beautifulsoup4

  • 建立 requirements.txt,專案裡所需套件的清單

    pip freeze > requirements.txt

  • 安裝 requirements.txt 清單的套件

    pip install -r requirements.txt

三、Python 爬蟲套件

BeautifulSoup

  • 安裝 beautifulsoup4

    pip install beautifulsoup4

  • 使用 BeautifulSoup 抓取資料
from bs4 import BeautifulSoup
import requests

# fetch url
fetchUrl = "https://baito.mynavi.jp/tokyo/ss-1/ssm-1/sss-1/kd-61/"
r = requests.get(fetchUrl)

# new BeautifulSoup
soup = BeautifulSoup(r.text, "html.parser")

# 用 css selector 取得 element
element = soup.select(".searchResult > .researchCount")
print(element)

Selenium + BeautifulSoup

  • 安裝 Selenium

    pip install selenium

  • 在 Linux 安裝 google chrome

    yum install https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm -y

  • Selenium 開起 chrome 抓取 dom,再用 BeautifulSoup 抓取資料。
from bs4 import BeautifulSoup
import time
from selenium import webdriver
from selenium.webdriver.common.by import By

# 啟動 driver
option = webdriver.ChromeOptions()

# 背景開啟瀏覽器
# option.add_argument('headless')

# 開啟瀏覽器
driver = webdriver.Chrome(option)

# fetch url
fetchUrl = "https://baito.mynavi.jp/cl-002280403865/job-94305516/?reserve_w_29450608"
driver.get(fetchUrl)

# 等待頁面渲染
time.sleep(1)

# tab 会社情報
button = driver.find_element(By.CSS_SELECTOR, ".tab02")
button.click()
time.sleep(3)

# 取得 dom
html = driver.page_source
soup = BeautifulSoup(html, 'html.parser')
address = soup.select(".workPlaceAddressDiamondMark")
for a in address:
    print(a.text)

PyMySQL 連線資料庫

import pymysql

db = pymysql.connect(host='',
                     user='',
                     password='',
                     database='')

cursor = db.cursor()

四、程式邏輯

  • 撈取一覽頁的邏輯:
    依規格書要求的件數撈取 200 件,從第一頁開始,直到滿足 200 件 break 迴圈。 要注意的是,若規格書要求 200 件,但實際上只有 199 件,也要 break 迴圈。

/urlList/melody.py

targetUrl = [
    {'url': 'https://job-medley.com/est/pref13/employment3/', 'number': 200},
    {'url': 'https://job-medley.com/ans/pref14/employment3/', 'number': 132},
    {'url': 'https://job-medley.com/ans/pref27/employment3/', 'number': 126},
    ...
]

fetchMelodyJob.py

from urlList import melody
import requests
from bs4 import BeautifulSoup

for idx, list in enumerate(melody.targetUrl):
    page = 1
    count = 0
    while count < list['number']:
        # 從第 1 頁開始 (https://job-medley.com/est/pref13/employment3/?page=2)
        fetchUrl = list['url'] if page == 1 else list['url'] + "?page=" + str(page)
        
        # request 之前 sleep 1~10 秒
        time.sleep(random.randint(1, 10))
        r = requests.get(fetchUrl)

        # new BeautifulSoup
        soup = BeautifulSoup(r.text, "html.parser")

        # 取得一覽頁總件數
        shopCount = soup.select(".c-search-condition-title__important")[0].text

        # 取得資訊
        shopBlock = soup.select(".c-offers-sort-area .c-offers-sort-area__contents .o-gutter-row .o-gutter-row__item")
        for sb in shopBlock:
            data = {
                "job_url": sb.select(".job-url"), # 店鋪詳細 url
                "station": sb.select(".station")  # 店鋪所在地
            }

            # 儲存資料庫
            insert_mysql(fetchUrl, data)
    
            # 計算筆數
            count += 1

            # 若滿足件數,繼續撈取下一個一覽頁
            if count >= list['number']:
                break

        # 若已達一覽頁總件數,繼續撈取下一個一覽頁
        if count >= int(shopCount):
            break
  • 撈取店鋪詳細頁的邏輯:
    依車站排序,撈取店舖 url。 Insert 資料庫,並 update flag 紀錄已經 fetch 過此店舖。

fetchMelodyJobDetail.py

import requests
from bs4 import BeautifulSoup

def select_jobs():
    sql = ("SELECT id, job_url, station, is_requested," +
           "CASE WHEN station LIKE '% 尼崎駅%' THEN 1 " +
           "     WHEN station LIKE '% 明石駅%' THEN 2 " +
           "     WHEN station LIKE '% 高松駅%' THEN 3 " +
           " FROM `melody`" +
           " WHERE is_requested = 0" +
           " ORDER BY ordering, id ASC" +
           " LIMIT 100")
           
def update_sql(id):
    try:
        sql = "UPDATE melody SET is_requested = 1 WHERE id = " + str(id)
        cursor.execute(sql)
        db.commit()
    except:
        write_log('sql update error: melody id - ' + str(id))
        db.rollback()

############################## main ##############################
jobs = select_jobs()
for job in jobs:
    #取得資訊
    fetchUrl = job['job_url']
    r = requests.get(fetchUrl, headers={'user-agent': 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'})
    soup = BeautifulSoup(r.text, "html.parser")
    ldJson = soup.select('script[type="application/ld+json"]')
    ...
    
    # 儲存資料庫
    insert_mysql(fetchUrl, data)
    
    # 更新資料表 is_requested = 1
    update_sql(job['id'])
import googlemaps

gmaps = googlemaps.Client(key="[MY_TOKEN]")
search_result = gmaps.places(facilityName)
tel = ""
if len(search_result['results']) > 0:
    # 先找到 <位置ID>
    place_id = search_result['results'][0]['place_id']

    # 用 <位置ID> 找店家詳細
    place_details = gmaps.place(place_id=place_id, fields=['name', 'formatted_phone_number'])
    tel = place_details['result'].get('formatted_phone_number')

    # 印出 tel
    print(tel)
r = requests.get(fetchUrl, headers={'user-agent': 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'})
  • 一覽頁 table 設計:
id fetch_url job_url station is_request
1 一覽頁 url 店鋪 A url 表参道駅 徒歩4分 1
2 一覽頁 url 店鋪 B url 日比谷駅 徒歩1分 0

五、遇到的問題

  • chrome 只有支援 x86 的 Linux。目前沒有支援 arm64 的 chrome,使用 node 的木偶人也是如此,木偶人官網有寫。

    uname -m

  • 建立爬蟲中斷機制,以免發生中斷又要從頭執行。我使用了 2 個方法:
    • 一覽頁的爬蟲:每次重新執行時,確認每個一覽頁已經 insert 了多少「店鋪件數」,已達件數便不再 fetch。

      SELECT count(*) FROM baito WHERE fetch_url LIKE ‘[一覽頁 url]%’

    • 店鋪詳細頁的爬蟲:table 新增一個欄位 (is_request),紀錄已經 fetch 過的 url,每次重新執行時,只 select 沒 fetch 過的 url。

      SELECT * FROM baito WHERE is_requested = 0