src/main.mjs
@@ -4,16 +4,28 @@
import proxy from "selenium-webdriver/proxy.js";
import axios from "axios";
import * as fs from "fs";
import path from "path";
import { Worker, isMainThread, parentPort, workerData, threadId } from 'worker_threads';
import { HttpsProxyAgent } from "https-proxy-agent";
import { resolve } from "path";
/*-------------读取配置---------------*/
let config = JSON.parse(fs.readFileSync('./config.json'));
/* ------------日志-------------- */
const _log = console.log;
const logFile = fs.createWriteStream('./logs.log');
console.log = function (text) {
  text = `${new Date().toLocaleString()} ${text ?? ''}`;
  _log(text);
  logFile.write(text + '\n');
};
let logFile;
function initLogger() {
  const _log = console.log;
  if (!fs.existsSync('./logs')) {
    fs.mkdirSync('./logs', { recursive: true });
  }
  logFile = fs.createWriteStream(`./logs/logs-${config.startRow}-${config.endRow}-thread${threadId}.log`, { flags: 'a', encoding: 'utf8' });
  console.log = function (...text) {
    text = `${new Date().toLocaleString()} ${text.join(' ') ?? ''}`;
    _log(text);
    logFile.write(text + '\n');
  };
}
/* ----------axios代理------------ */
const httpsAgent = new HttpsProxyAgent(`http://127.0.0.1:10809`);
@@ -61,6 +73,12 @@
 */
async function createDriver() {
  const opts = new ChromeOptions();
  if (config.headless) {
    opts.addArguments("--headless");//开启无头模式
  }
  if (config.disableGpu) {
    opts.addArguments("--disable-gpu");//禁止gpu渲染
  }
  opts.addArguments("--ignore-ssl-error"); // 忽略ssl错误
  opts.addArguments("--no-sandbox"); // 禁用沙盒模式
  opts.addArguments("blink-settings=imagesEnabled=false"); //禁用图片加载
@@ -77,14 +95,21 @@
/**
 * 格式化关键字
 * @param {string} text 要搜索的关键字
 * @param {boolean} titleWithNumbers 是否标题中包含数字
 * @returns 处理后的关键字
 */
function formatKw(text) {
  // 只保留中文、英文、数字和下划线
  return text.replace(/[^\u4e00-\u9fa5\w \d]/g, "");
function formatKw(text, titleWithNumbers) {
  // 只保留空格、中文、英文、法文、德文、希腊文
  const regex = /[^\u4e00-\u9fa5\w\s\d]/g;
  if (titleWithNumbers) {
    text = text.replace(/[^\u4e00-\u9fa5a-zA-Z\u00c0-\u024f \d]/g, "");
  } else {
    text = text.replace(/[^\u4e00-\u9fa5a-zA-Z\u00c0-\u024f ]/g, "");
  }
  text = text.split(' ').slice(0, 10).join("+");
  return text;
}
const driver = await createDriver();
async function sleep(ms) {
  return new Promise((resolve) => {
@@ -109,11 +134,11 @@
 * 打开搜索页面并搜索
 * @param {*} book 
 */
async function openSearchPage(book) {
  console.log(`打开搜索: https://archive.org/search?query=${formatKw(book.title)}`);
async function openSearchPage(book, titleWithNumbers) {
  console.log(`打开搜索: https://archive.org/search?query=${formatKw(book.title, titleWithNumbers)}`);
  return await retry(async () => {
    // 获取页面
    const searchUrl = `https://archive.org/search?query=${formatKw(book.title)}`;
    const searchUrl = `https://archive.org/search?query=${formatKw(book.title, titleWithNumbers)}`;
    await driver.get(searchUrl);
  }).then(() => true)
    .catch(() => false);
@@ -167,7 +192,7 @@
    await driver.wait(
      until.elementLocated(
        By.xpath(`//*[@id="maincontent"]/div[5]/div/div/div[2]/section[2]/div`)
      )
      ), 15000
    );
  })
    .then(() => true)
@@ -223,14 +248,32 @@
async function downloadFile(book, url) {
  console.log(`下载文件: ${url}`);
  const ext = url.split(".").pop();
  const filepath = `./downloads/${book.id} ${book.isbn}.${ext}`;
  if (fs.existsSync(filepath)) {
    book.state = `下载完成`;
    book.format = ext;
    book.file = filepath;
    book.url = url;
    console.log(`下载完成:${filepath}`);
    return;
  }
  await retry(() => {
    return new Promise((resolve, reject) => myAxios
      .get(url, { responseType: "stream" })
      .then((response) => {
        const len = response.headers['content-length'];
        if (ext !== "pdf" && ext !== "txt" && len > 200 * 1024 * 1024) {
          // 不是pdf或txt文件,且文件大于200M,不下载
          book.state = "下载失败";
          book.url = url;
          console.log(`下载失败: ${book.id} ${book.title} ${url}`);
          reject(false);
          return;
        }
        const stream = response.data;
        const ext = url.split(".").pop();
        const filepath = `./downloads/${book.id} ${book.isbn}.${ext}`;
        stream.pipe(fs.createWriteStream(filepath));
        const out = fs.createWriteStream(filepath);
        stream.pipe(out);
        stream.on("end", () => {
          book.state = `下载完成`;
          book.format = ext;
@@ -239,30 +282,83 @@
          console.log(`下载完成:${filepath}`);
          resolve(true);
        });
        stream.on("error", (err) => {
          console.error(err);
          book.state = "下载失败";
          book.url = url;
          console.log(`下载失败: ${book.id} ${book.title} ${url}`);
          reject(false);
          try {
            out.close();
            fs.unlink(filepath, (e) => console.error(e));
          } catch (e) {
            console.error(e);
          }
        });
      })
      .catch((e) => {
        console.error(e);
        book.state = "下载失败";
        book.url = url;
        console.log(`下载失败: ${book.id} ${book.title}`);
        console.log(`下载失败: ${book.id} ${book.title} ${url}`);
        reject(false);
      }));
  }).catch(e => {
    return false
  });
}
function isAlreadyDownloaded(book) {
  const id = `${book.id} ${book.isbn}`;
  return alreadyDownloadedBooks.includes(id);
}
function nextBook() {
  return new Promise(resolve => {
    const cb = (message) => {
      if (message.type === 'book') {
        resolve(message.data);
        parentPort.removeListener('message', cb);
      }
    };
    parentPort.on('message', cb);
    parentPort.postMessage({ type: 'get-book', threadId });
  });
}
async function downloadBooks(books) {
  for (const book of books) {
    if (book.state && (book.state === "没有搜索结果" || book.state === "没有pdf或text文件")) {
      // 跳过没有搜索结果或没有pdf或text文件的书籍
      continue;
  driver = await createDriver();
  for (; ;) {
    const book = await nextBook();
    if (!book) {
      break;
    }
    books.push(book);
    if (config.endOfTime && Date.now() - startTime > 1000 * 60 * config.endOfTime) {
      // 定时退出
      break;
    }
    bookCount++;
    if (isAlreadyDownloaded(book)) {
      skipCount++;
      continue;
    }
    if (book.state && (book.state === "没有搜索结果" || book.state === "没有pdf或text文件" || book.state === "下载完成")) {
      // 跳过没有搜索结果或没有pdf或text文件的书籍
      skipCount++;
      continue;
    }
    console.log(`开始下载: ${book.id} ${book.title}`);
    // 打开搜索页面并搜索
    if (!await openSearchPage(book)) {
      console.log(`打开搜索页面失败: ${book.id} ${book.title}`);
      book.state = "打开搜索页面失败";
      continue;
    if (!await openSearchPage(book, true)) {
      // 先用包含数字的关键字,如果没有结果再用不包含数字的关键字
      if (!await openSearchPage(book, false)) {
        console.log(`打开搜索页面失败: ${book.id} ${book.title}`);
        book.state = "打开搜索页面失败";
        continue;
      }
    }
    // 检测搜索结果
    const hasBook = await checkSearchResult(book);
@@ -277,30 +373,30 @@
      continue;
    }
    // 等一段时间再打开详情页
    sleep(getRandomNumber(3000, 10000));
    sleep(getRandomNumber(1000, 30000));
    // 打开详情页
    await openBookDetailPage(book, detailPageUrl);
    // 获取下载链接
    const url = await getDownloadUrl(book);
    if (!url) { continue; }
    // 等待一段时间再下载
    await sleep(getRandomNumber(3000, 10000));
    await sleep(getRandomNumber(1000, 30000));
    // 下载文件
    await downloadFile(book, url);
    console.log(`下载完成: ${book.id} ${book.title}`);
    try {
      await downloadFile(book, url);
      console.log(`下载完成: ${book.id} ${book.title}`);
    } catch (e) { }
    successCount++;
    // 等一段时间再下一个
    sleep(getRandomNumber(3000, 10000));
    sleep(getRandomNumber(1000, 30000));
  }
  await driver.close();
  await driver.quit();
}
function saveBooks(books) {
  console.log("保存下载状态数据");
  const workSheets = xlsx.parse("【第二批二次处理后】交付清单.xlsx");
  const sheet = workSheets[0];
  const data = sheet.data.slice(2);
  const data = sheet.data;
  for (const book of books) {
    const index = data.findIndex((row) => row[0] === book.id);
    if (index > -1) {
@@ -312,7 +408,7 @@
  }
  const buffer = xlsx.build([{ name: "Sheet1", data }]);
  fs.writeFile("./【第二批二次处理后】交付清单.xlsx", buffer, (err) => { });
  fs.writeFileSync("./【第二批二次处理后】交付清单.xlsx", buffer, (err) => { });
  console.log("保存完成: ./【第二批二次处理后】交付清单.xlsx");
}
@@ -346,18 +442,72 @@
let successCount = 0;
// 图书数量
let bookCount = 0;
// 跳过的数量,已经下载过或没有搜索到的数量
let skipCount = 0;
// chrome驱动
let driver;
let alreadyDownloadedBooks = [];
function getAlreadyDownloadedBooks() {
  const text = fs.readFileSync('./alreadyDownloadedBooks.txt', 'utf-8');
  const books = text.replace(/\r/g, '').split('\n').map(it => it.trim()).filter(it => it);
  const files = fs.readdirSync('./downloads');
  books.push(...files);
  return books.map(it => path.basename(it, path.extname(it)).trim());
}
function main() {
  const range = JSON.parse(fs.readFileSync('./config.json'));
  const books = getBooksFromExcel(range.startRow, range.endRow);
  initLogger();
  const books = [];
  downloadBooks(books)
    .then(() => {
      console.log(`全部完成,共下载${bookCount}本,成功下载${successCount}本,失败${bookCount - successCount}本,耗时: ${msFormat(Date.now() - startTime)}。`);
      console.log(`全部完成,共下载${bookCount}本,成功下载${successCount}本,跳过${skipCount}本,失败${bookCount - skipCount - successCount}本,耗时: ${msFormat(Date.now() - startTime)}。`);
    })
    .finally(() => {
      saveBooks(books);
    .catch(e => {
      console.error(e);
    })
    .finally(async () => {
      // saveBooks(books);
      parentPort.postMessage({ type: "books", data: books });
      logFile.close();
      try {
        await driver.close();
        await driver.quit();
      } catch (e) { }
    });
}
main();
// 多进程执行
if (isMainThread) {
  initLogger();
  const alreadyDownloadedBooks = getAlreadyDownloadedBooks();
  const { startRow, endRow, threadSize } = config;
  console.log(`线程数:${threadSize}, 开始行:${startRow}, 结束行:${endRow}`);
  let finishCnt = 0;
  const finishBooks = [];
  const thBookSize = (endRow - startRow) / threadSize;
  const books = getBooksFromExcel(startRow, endRow);
  for (let sr = startRow; sr < endRow; sr += thBookSize) {
    let er = sr + thBookSize;
    if (er > endRow) {
      er = endRow;
    }
    const worker = new Worker("./src/main.mjs", { workerData: { startRow: sr, endRow: er, alreadyDownloadedBooks } });
    worker.on("message", (message) => {
      if (message.type === 'books') {
        finishBooks.push(...message.data);
        finishCnt++;
        if (finishCnt >= threadSize) {
          saveBooks(finishBooks);
        }
      } else if (message.type === 'get-book') {
        worker.postMessage({ type: "book", data: books.shift() });
      }
    });
  }
} else {
  alreadyDownloadedBooks = workerData.alreadyDownloadedBooks;
  main();
}