Krixter commited on
Commit
cfbd298
ยท
verified ยท
1 Parent(s): 6d0e200

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +121 -117
app.py CHANGED
@@ -1,121 +1,125 @@
1
- import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import requests
3
  from bs4 import BeautifulSoup
4
- import os
5
- import zipfile
6
- import shutil
7
-
8
- def scrape_novels(start_page, end_page, progress=gr.Progress()):
9
- base_url = "https://www.novelhall.com/all2022-{}.html"
10
-
11
- # Reset and prepare the HTML directory for a fresh scrape
12
- html_dir = "html_pages"
13
- if os.path.exists(html_dir):
14
- shutil.rmtree(html_dir)
15
- os.makedirs(html_dir, exist_ok=True)
16
-
17
- combined_txt_path = "combined_novels.txt"
18
- headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
19
-
20
- with open(combined_txt_path, 'w', encoding='utf-8') as combined_file:
21
-
22
- # Iterate over the specified page range
23
- for page in progress.tqdm(range(int(start_page), int(end_page) + 1), desc="Scraping List Pages"):
24
- page_url = base_url.format(page)
25
- try:
26
- response = requests.get(page_url, headers=headers, timeout=15)
27
- soup = BeautifulSoup(response.content, 'html.parser')
28
-
29
- # Extract all novel links from the current list page
30
- links = []
31
- for li in soup.find_all('li', class_='btm'):
32
- a_tag = li.find('a')
33
- if a_tag and 'href' in a_tag.attrs:
34
- href = a_tag['href']
35
- if href.startswith('/'):
36
- href = "https://www.novelhall.com" + href
37
- links.append(href)
38
-
39
- # Visit each novel link individually
40
- for link in links:
41
- try:
42
- novel_response = requests.get(link, headers=headers, timeout=15)
43
- novel_soup = BeautifulSoup(novel_response.content, 'html.parser')
44
-
45
- # 1. Extract Title
46
- title = "Unknown Title"
47
- info_div = novel_soup.find('div', class_='book-info')
48
- if info_div and info_div.find('h1'):
49
- title = info_div.find('h1').text.strip()
50
- elif novel_soup.find('h1'):
51
- title = novel_soup.find('h1').text.strip()
52
-
53
- # 2. Extract Description (Ensuring we don't scrape the truncated version)
54
- description = "Unknown Description"
55
- intro_div = novel_soup.find('div', class_='intro')
56
- if intro_div:
57
- close_wrap = intro_div.find('span', class_='js-close-wrap')
58
- if close_wrap:
59
- # Remove the "back<<" span text
60
- back_span = close_wrap.find('span', class_='blue')
61
- if back_span:
62
- back_span.decompose()
63
- description = close_wrap.text.strip()
64
- else:
65
- description = intro_div.text.strip()
66
-
67
- # 3. Save the HTML file locally
68
- # Sanitize title to ensure the filename is valid
69
- safe_title = "".join(c for c in title if c.isalnum() or c in " -_").rstrip()
70
- if not safe_title:
71
- safe_title = link.strip('/').split('/')[-1]
72
-
73
- html_filename = f"{html_dir}/{safe_title}.html"
74
- with open(html_filename, 'w', encoding='utf-8') as html_file:
75
- html_file.write(novel_response.text)
76
-
77
- # 4. Write data to the Combined TXT File
78
- combined_file.write(f"Title: {title}\n")
79
- combined_file.write(f"Link: {link}\n")
80
- combined_file.write(f"Description:\n{description}\n")
81
- combined_file.write("-" * 80 + "\n\n")
82
-
83
- except Exception as e:
84
- combined_file.write(f"Error scraping novel {link}: {e}\n\n")
85
-
86
- except Exception as e:
87
- combined_file.write(f"Error scraping list page {page_url}: {e}\n\n")
88
-
89
- # Compress the text file and HTML directories into a downloadable ZIP
90
- zip_path = "scraped_data.zip"
91
- with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
92
- zipf.write(combined_txt_path, arcname=combined_txt_path)
93
- for root, _, files in os.walk(html_dir):
94
- for file in files:
95
- file_path = os.path.join(root, file)
96
- zipf.write(file_path, arcname=os.path.join(html_dir, file))
97
-
98
- return zip_path, f"Successfully processed pages {start_page} to {end_page}!"
99
-
100
- # Set up the UI logic
101
- with gr.Blocks(title="Novelhall Web Scraper") as demo:
102
- gr.Markdown("# ๐Ÿ“š Novelhall Scraper Application")
103
- gr.Markdown("Extracts individual novel links, saves their HTML structure, and compiles titles and full descriptions into a combined text file. **Note:** Large ranges take a long time to run; scrape in batches to prevent server timeouts.")
104
-
105
- with gr.Row():
106
- start_pg = gr.Number(label="Start Page", value=1, precision=0)
107
- end_pg = gr.Number(label="End Page", value=2, precision=0)
108
-
109
- scrape_btn = gr.Button("Start Scraping", variant="primary")
110
-
111
- output_msg = gr.Textbox(label="Status Tracker")
112
- output_file = gr.File(label="Download Archive (Contains HTMLs & Combined TXT)")
113
-
114
- # Link the function
115
- scrape_btn.click(
116
- fn=scrape_novels,
117
- inputs=[start_pg, end_pg],
118
- outputs=[output_file, output_msg]
119
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
- demo.launch()
 
 
1
+ """
2
+ WebNovel link scraper
3
+ ----------------------
4
+ Crawls WebNovel's genre/tag/search listing pages and collects every unique
5
+ novel ("/book/...") link it finds, then writes them all to a .txt file.
6
+
7
+ Run on Hugging Face (Space / Notebook / Colab-style env) or locally:
8
+ pip install requests beautifulsoup4
9
+ python webnovel_link_scraper.py
10
+
11
+ Notes:
12
+ - WebNovel loads a lot of its catalog dynamically via JS/AJAX. This script
13
+ only sees server-rendered HTML, so it will pick up a large subset of
14
+ novels (everything reachable from listing/search pages) rather than
15
+ literally every title on the site.
16
+ - Be a good citizen: there's a delay between requests, and you should check
17
+ https://www.webnovel.com/robots.txt and their Terms of Service before
18
+ running this at scale or pointing it at many pages.
19
+ """
20
+
21
+ import re
22
+ import time
23
+ import random
24
+ from urllib.parse import urljoin
25
+
26
  import requests
27
  from bs4 import BeautifulSoup
28
+
29
+ BASE_URL = "https://www.webnovel.com"
30
+ OUTPUT_FILE = "webnovel_links.txt"
31
+
32
+ HEADERS = {
33
+ "User-Agent": (
34
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
35
+ "(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  )
37
+ }
38
+
39
+ # A handful of starting points that list lots of novels.
40
+ # Add/remove genre or tag slugs as you like.
41
+ SEED_PATHS = [
42
+ "/category/all",
43
+ "/category/all?orderBy=0", # popularity sort, if supported
44
+ "/category/all?orderBy=1", # other sort options
45
+ "/tags/fantasy",
46
+ "/tags/romance",
47
+ "/tags/action",
48
+ "/tags/adventure",
49
+ "/tags/r18",
50
+ "/tags/system",
51
+ ]
52
+
53
+ # Matches links like /book/some-novel-title_12345678905012345
54
+ BOOK_LINK_RE = re.compile(r"^/book/[\w\-]+$")
55
+
56
+ MAX_PAGES_PER_SEED = 20 # how many "page=" pages to walk per seed URL
57
+ REQUEST_DELAY_RANGE = (1.0, 2.5) # polite random delay between requests
58
+
59
+
60
+ def fetch(url: str) -> str | None:
61
+ try:
62
+ resp = requests.get(url, headers=HEADERS, timeout=15)
63
+ if resp.status_code == 200:
64
+ return resp.text
65
+ print(f" [!] {resp.status_code} for {url}")
66
+ except requests.RequestException as e:
67
+ print(f" [!] request failed for {url}: {e}")
68
+ return None
69
+
70
+
71
+ def extract_book_links(html: str) -> set[str]:
72
+ soup = BeautifulSoup(html, "html.parser")
73
+ found = set()
74
+ for a in soup.find_all("a", href=True):
75
+ href = a["href"]
76
+ # Normalize relative/absolute links pointing at /book/...
77
+ if href.startswith(BASE_URL):
78
+ href = href[len(BASE_URL):]
79
+ if BOOK_LINK_RE.match(href):
80
+ found.add(urljoin(BASE_URL, href))
81
+ return found
82
+
83
+
84
+ def crawl():
85
+ all_links: set[str] = set()
86
+
87
+ for seed in SEED_PATHS:
88
+ print(f"\n=== Seed: {seed} ===")
89
+ for page in range(1, MAX_PAGES_PER_SEED + 1):
90
+ sep = "&" if "?" in seed else "?"
91
+ url = f"{BASE_URL}{seed}{sep}pageIndex={page}"
92
+ print(f"Fetching {url}")
93
+
94
+ html = fetch(url)
95
+ if not html:
96
+ break
97
+
98
+ new_links = extract_book_links(html)
99
+ before = len(all_links)
100
+ all_links.update(new_links)
101
+ added = len(all_links) - before
102
+ print(f" found {len(new_links)} links on page, {added} new")
103
+
104
+ # Stop walking this seed once a page yields nothing new
105
+ if added == 0 and page > 1:
106
+ break
107
+
108
+ time.sleep(random.uniform(*REQUEST_DELAY_RANGE))
109
+
110
+ return all_links
111
+
112
+
113
+ def main():
114
+ links = crawl()
115
+ print(f"\nTotal unique novel links collected: {len(links)}")
116
+
117
+ with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
118
+ for link in sorted(links):
119
+ f.write(link + "\n")
120
+
121
+ print(f"Saved to {OUTPUT_FILE}")
122
+
123
 
124
+ if __name__ == "__main__":
125
+ main()