# coding=utf-8

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urldefrag
from multiprocessing import Pool


def funcURLtoHTML(myURL):                                           # 抓取指定 URL 的网页，将其作为一个字符串返回
    while True:
        try:
            myResponse = requests.get(myURL, allow_redirects=False, timeout=5)
        except:
            print("ERROR")
        else:
            if myResponse.status_code == 200:                      # 成功抓取
                break
            elif myResponse.status_code == 500:                    # 修复有问题的 URL 并重新再抓
                myURL = myURL.split("<")[0]
            else:
                log = f"{myResponse.status_code} | {myURL}"        # 遇到其它错误也重新再抓
                print(log)

    return myResponse.text


def funcSoupToExcerpt(mySoup):                                     # 提取一个网页中的词条内容，合并成一个字符串返回
    myHTMLexcerptMerged = ""
    myHTMLexcerptSet = mySoup.select("div.entryRH")                # 提取网页中所有的<div class="entryRH ...">内容，结果放在一个 set 中（可在Chrome用右键>Inspect查看html的DOM结构）

    for myExcept in myHTMLexcerptSet:                              # 合并提取到的词条内容成一个单一字符串
        myHTMLexcerptMerged += str(myExcept)

    myHTMLexcerptMerged = myHTMLexcerptMerged.replace("\n", "")    # 字符串尾部加一个换行符

    return myHTMLexcerptMerged


def funcCrawl(myURLSetNumbered):                                   # 抓取一组网页，参数格式：[ Num, URL Set ]
    myThreadN, myURLs = myURLSetNumbered
    myURLsHarvested = set()                                        # 创建一个空的 set 用于存放抓取一组 URL 得到的一组网页

    for myURL in myURLs:
        myHTML = funcURLtoHTML(myURL)                              # 抓取一个URL网页的原始版本
        mySoup = BeautifulSoup(myHTML, "html5lib")                 # 传递网页原始版本给 bs4 供解析之用，此处用到的 html5lib 解析器不在bs4中，需额外安装

        for myHref in mySoup.select("a[href]"):                    # 收集一个URL网页上的 See Also 词条链接
            myURLbase = "https://www.wordreference.com/definition/"
            myURLnew = urldefrag(urljoin(myURLbase, myHref["href"])).url
            if myURLnew.startswith(myURLbase):
                myURLsHarvested.add(myURLnew)
                print(myURLnew)

        myHTMLexcerpt = funcSoupToExcerpt(mySoup)                   # 从网页原始版本中提取词条的部分
        if myHTMLexcerpt:
            myFile = f"Thread_{myThreadN}.txt"                      # 每个线程的处理结果写入一个独立的文本文件，这个文件和本Python脚本位于同一文件夹下
            with open(myFile, encoding="utf-8", mode="a") as myStream:
                myStream.write(f"{myHTMLexcerpt}\n")

    return myURLsHarvested                                          # 保存提取到的网页内容后，返回本次处理网页收集的新词条链接


def subCrawlBy8threads(myURLsToCrawl, myURLBankSoFar):              # 把一组网页平均分成8组以便同时用8个线程进行抓取。参数：myURLsToCrawl=本次需抓取的url列表, myURLBankSoFar=目前为止收集到的全部url列表
    myNumOfThreads = 8
    myURL8Sets = [set() for i in range(myNumOfThreads)]             # 创建 myURL8Sets 为含有 8 个空的 set 的 list

    for myNum, myURL in enumerate(myURLsToCrawl):                   # 把 myURLsToCrawl 中的所有 url 成员分成8组，给8个线程用
        myURL8Sets[myNum % myNumOfThreads].add(myURL)

    with Pool(myNumOfThreads) as myPool:
        myURLSetsHarvested = myPool.map(funcCrawl, enumerate(myURL8Sets))  # 用 8个线程同时把 8组 url 传递给 funcCrawl 进行抓取，并收集返回的 See Also 词条链接

    myURLsHarvested = set().union(*myURLSetsHarvested)               # 把得到的 8 组结果合并到一个 myURLsHarvested 中

    myURLsToCrawl = myURLsHarvested - myURLBankSoFar                 # 求差：把新收集到的词条链接中已经下载过的那些剔除掉
    myURLBankSoFar = myURLsHarvested | myURLBankSoFar                # 求和：把心收集到的词条链接加入URL总库 myURLBankSoFar 中

    if myURLsToCrawl:
        subCrawlBy8threads(myURLsToCrawl, myURLBankSoFar)            # 递归：继续抓取新收集到的词条链接


if __name__ == "__main__":                                           # 运行入口
    mySeedURL = {"https://www.wordreference.com/definition/accord"}  # 提供给程序一个种子词条的网址，以此为起点开始抓取
    subCrawlBy8threads(mySeedURL, mySeedURL)
