attoveker
注意:專案內容會爬取18禁成人色情片網站,但技術細節不會出現成人內容。
有時候我們必須承認自己就是會去看色情片。其中我發現 attackers.net 的網頁架構相當好、內容也非常完整,很有做API的條件。
爬取
爬取其實蠻簡單的:
- 抓網頁
- 抓資料
- 存資料
當然如果有 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;
}
我無言。花了這麼久卻是白費一場。
小節
暫時先這樣吧。東西還很多呢?