attoveker

注意:專案內容會爬取18禁成人色情片網站,但技術細節不會出現成人內容。

有時候我們必須承認自己就是會去看色情片。其中我發現 attackers.net 的網頁架構相當好、內容也非常完整,很有做API的條件。

爬取

爬取其實蠻簡單的:

  1. 抓網頁
  2. 抓資料
  3. 存資料

當然如果有 API 就更好了。雖然很多人都用 Python 爬取資料,但個人習慣使用 JavaScript 以及各種 WebAPI 爬取資料。

好了,不多說了。來跑吧。

Typescript

首先,因為網站架構相當有規律,所以感覺可以做成 typescript。不過實做上,卻與我想得很不一樣:我以為可以直接用 typescript 跑 express,但事實上,我們需要先把程式 build 成 JavaScript 檔,然後用它跑程式。

嘖,該怎說呢,蠻雞肋的。寫起來可能感覺有點安全,但多了一個 build 的步驟,卻是讓每次改完都要重開的 express 變得更麻煩。真不曉得該如何是好。

卡片

attackers.net 的影片大都以這種方式組成:

<div class="item">
    <div class="c-card">
        <a class="img hover" href="https://attackers.net/works/detail/{ID}">
            <img class="c-main-bg lazyload" data-src="URL" alt="" src="URL">
            <div class="hover__child">
                <p class="text">片名</p>
                <div class="arrow">
                    <div class="c-svg">
                        <!-- SVG 導覽圖。在此省略。 -->
                    </div>
                </div>
            </div>
        </a>
        <a class="name c-main-font-hover" href="https://attackers.net/actress/detail/{ID}">女優</a>
    </div>
</div>

所以要抓非常好抓:把 querySelector 的選項指定為 .item .c-card 就可以了。接下來就是處理各種圖片啥的。

export class ImageCards extends DomList {
    dom_name = ".item .c-card"
    constructor(input: Document) {
        super();
        if( input ) {
            this.set_list_by_dom(input);
        }
    }
    get_id( regex = /whatever/g, input = "" ) {
        const get_text = (regex = /whatever/g, input = "") => {
            // Use the regex to extract the ID from the text
            const match = input.match(regex);
            // Check if a match is found
            if (match) {
                // Return the captured the ID
                return match[0] ? match[0] : "";
            } else {
                // Return null if no match is found
                return "";
            }
        };
        return get_text(regex, input);
    }
    get_actor(its: Element): ActorInterface {
        return getActorData(its);
    }
    api() {
        return this.list.map( (its) => {
            const link_dom: HTMLAnchorElement | null = its.querySelector("a");
            const image_dom: HTMLImageElement | null = its.querySelector("img.c-main-bg");
            const title_dom: HTMLParagraphElement | null = its.querySelector("p.text");
            return {
                image: image_dom ? image_dom.dataset.src : "",
                title: title_dom ? title_dom.textContent : "",
                link: link_dom ? link_dom.href : "",
                id: this
                    .get_id( /\/works\/detail\/([A-Z0-9]+)(\?|)/g, link_dom ? link_dom.href : "")
                    .replace(/\/works\/detail\//g, ""),
                actor: this.get_actor(its),
            };
        });
    }
}

const cards = new ImageCards(document);
cards.api();

WebAPI 抓資料其實有點累……也或許只是我需要的 API 太多、太有野心了。

連結

我以為去 parse <a href="https://attackers.net/actress/detail/351897">松下紗栄子</a> 這樣的連結然後轉成 { name: "松下紗栄子", id: "351897", link: "https://attackers.net/actress/detail/351897" } 應該很簡單。

但我錯了:只要東西一多,工作量界會急遽增加。

首先,光是要確保程式能抓到 a 就不容易:有些元件根本就沒有這玩意。所以要找到連結,就需要好好看網頁的架構、並需要花時間去確認。

接著要抓連結內的 ID 就更困難了。看看一個網站可以有哪些連結吧:

<a href="https://attackers.net/works/detail/ATVR053?page_from=actress&sys_code=123456">ATVR053</a>
<a href="https://attackers.net/actress/detail/354850">二宮ひかり</a>
<a href="https://attackers.net/works/list/genre/447">ドラマ</a>
<a href="https://attackers.net/works/list/series/2327">女優BEST</a>
<a href="https://attackers.net/works/list/label/9455">大人のドラマ</a>
<a href="https://attackers.net/works/list/date/2024-05-07">2024年5月7日</a>

噢,拜託。

const get_detection_regex = (type: GetDataType) => {
    switch (type) {
        case GetDataType.Actor: return /\/actress\/detail\/([0-9]+)(\?|)/g;
        case GetDataType.Genre: return /\/works\/list\/genre\/([0-9]+)(\?|)/g;
        case GetDataType.Label: return /\/works\/list\/label\/([0-9]+)(\?|)/g;
        case GetDataType.Series: return /\/works\/list\/series\/([0-9]+)(\?|)/g;
        default: return /\w/g;
    }
};
const get_replacing_regex = (type: GetDataType) => {
    switch (type) {
        case GetDataType.Actor: return /\/actress\/detail\//g;
        case GetDataType.Genre: return /\/works\/list\/genre\//g;
        case GetDataType.Label: return /\/works\/list\/label\//g;
        case GetDataType.Series: return /\/works\/list\/series\//g;
        default: return /\w/g;
    }
};
const id = get_text( get_detection_regex(type), dom.href ).replace( get_replacing_regex(type), "" );

const video_id = this.get_id( /\/works\/detail\/([A-Z0-9]+)(\?|)/g, link_dom.href).replace(/\/works\/detail\//g, ""),

每次都要手動用 regex 抓取,可真教人無法忍受。

啊對還有日期……


最後我放棄,把測試檔案寫給 ChatGPT 看看能怎辦。想不到這小子還真有辦法:

function GetLinkId(url) {
    // Split the URL by '/'
    const parts = url.split('/');

    // Extract the last part of the URL
    const lastPart = parts[parts.length - 1];

    // Check if the last part contains any query parameters
    const queryIndex = lastPart.indexOf('?');

    // If query parameters exist, remove them
    const id = queryIndex !== -1 ? lastPart.substring(0, queryIndex) : lastPart;

    return id;
}

我無言。花了這麼久卻是白費一場。

小節

暫時先這樣吧。東西還很多呢?

參考資料