查看原文
其他

Python脚本批量下载创世纪图书馆电子书

carry麦 码农真经 2023-12-25

文章内容

1. 创世纪图书馆简介

2. Libgen.py及使用方法

3. 批量下载

创世纪图书馆(英语: Library Genesis,缩写为LibGen)是科学论文及书籍的搜索引擎,可以免费提供被挡在付费墙(paywall)后的内容。截止2018年6月,创世纪图书数据库宣称其包含超过270万册图书,以及超过5800万篇期刊文献。

这图书量杠杠的,称为创世纪不为过。

创世纪图书馆


打开网站首页:http://libgen.rs/。
(网站有很入口,建议搜索引擎搜索 libgen即可)


网站电子书很多,有时候搜索出来是一个列表,一个个下载非常麻烦。
如,搜索 “盗火者译丛”,返回该系列图书的列表。
http://libgen.lc/search.php?req=盗火者译丛&open=0&res=25&view=simple&phrase=1&column=def

返回的图书有18本,一个个点进去下载,要重复18次。

libgen.py


于是我写了个叫 libgen.py 的脚本来解析这些电子书的下载链接。


#! /usr/bin/env python3# coding: utf-8
__author__ = 'ChenyangGao <https://chenyanggao.github.io/>'__version__ = (0, 2, 4)__requirements__ = ['requests', 'urllib3', 'lxml', 'yarl']
import requestsimport urllib3 # type: ignore
from functools import partial, update_wrapperfrom itertools import chainfrom re import compile as re_compilefrom typing import ( cast, Callable, Generator, List, Optional, Tuple, Type, Union, )from urllib.parse import parse_qsl
from lxml.html import fromstring # type: ignorefrom yarl import URL

__all__ = [ 'AWAILABLE_ORIGINS', 'query', 'gen_query', 'query_info', 'query_downlink',]

# Reference: [Library Genesis Guide](https://librarygenesis.net/)AWAILABLE_ORIGINS: List[str] = [ 'http://gen.lib.rus.ec', 'http://libgen.rs', 'https://libgen.rs', 'http://libgen.is', 'https://libgen.is', 'http://libgen.st', 'https://libgen.st',]
MAX_TIMES: int = 5

class FetchFailed(Exception):
def __init__(self, *args, **kwds): super().__init__(*args) self.kwds = kwds
def __str__(self) -> str: return ', '.join(chain( (f'{arg}' for arg in self.args), (f'{k}={v!r}' for k, v in self.kwds.items()), ))
def __repr__(self) -> str: return f'{type(self).__name__}({self!s})'

class DownloadLinkNotFound(FetchFailed): pass

class AggregationException(Exception):
def __init__(self, *args, exceptions=(), **kwds): super().__init__(*args) self.exceptions = exceptions self.kwds = kwds
def __str__(self) -> str: return ', '.join(chain( (f'{arg}' for arg in self.args), (f'{k}={v!r}' for k, v in self.kwds.items()), (f'exceptions={self.exceptions!r}',), ))
def __repr__(self) -> str: return f'{type(self).__name__}({self!s})'

def retry( func: Optional[Callable] = None, times: Optional[int] = None, exceptions: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = Exception, custom_exception: Optional[Callable[..., BaseException]] = None, tackle: Optional[Callable] = None, tackle_exception: Callable[..., BaseException] = BaseException,) -> Callable: if func is None: return partial( retry, times=times, exceptions=exceptions, custom_exception=custom_exception, tackle=tackle, tackle_exception=tackle_exception, ) func = cast(Callable, func) if times is not None and times < 1: return func def wrapper(*args, **kwargs): t: int = MAX_TIMES if times is None else times if t < 1: return func(*args, **kwargs) excs: List[BaseException] = [] for _ in range(t): try: r = func(*args, **kwargs) if tackle and tackle(r): raise tackle_exception return r except exceptions as exc: excs.append(exc) if custom_exception: raise custom_exception(*args, **kwargs) else: raise AggregationException(args=args, kwargs=kwargs, exceptions=excs) return update_wrapper(wrapper, func)

@retry( exceptions=(urllib3.exceptions.HTTPError, requests.exceptions.HTTPError), custom_exception=FetchFailed, tackle=lambda r: b'<title>404</title>' in r.content, tackle_exception=requests.exceptions.HTTPError,)def fetch(url, data=None, **kwargs): if data is None: r = requests.get(url, **kwargs) else: r = requests.post(url, data=data, **kwargs) r.raise_for_status() return r

def absolute_url(url, context_url): if url.startswith(('.', '/')): if not isinstance(context_url, URL): context_url = URL(context_url) return str(context_url.with_path(url)) return url

def query( query_params: Union[str, dict], only_md5: bool = False, origin: str = 'http://libgen.rs', _cre=re_compile(r'(?<=book/index.php\?md5=)[^"\']+'),) -> list: '''通过一些查询参数,查询相关书本的列表信息
:param query_params: 查询参数,就是网页接口可接受的查询参数 :param only_md5: 是否只需要书本的 md5 :param origin: 源 [Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin)
:return: 如果 only_md5 为真,返回 list[str],是书本 md5 的列表; 否则,返回 list[dict],是书本的列表信息 ''' def extract_title(el): info = {} a = el.find('./a[1]') if a.text is None: info['series_href'] = a.attrib['href'] info['series'] = a.text_content() a = el.find('./a[2]') info['title_href'] = a.attrib['href'] info['title'] = a.text el = el.getnext() if el.tag != 'br': info['title_volumn'] = el.text_content() el = el.getnext() el = el.getnext() info['title_isbn'] = el.text_content() return info
if isinstance(query_params, str): url = f'{origin}/search.php?{query_params}' r = fetch(url) else: url = f'{origin}/search.php' r = fetch(url, params=query_params)
if only_md5: return _cre.findall(r.text)
tree = fromstring(r.content) table = tree.find('.//table[3]')
return [{ 'ID': el[0].text, 'Author(s)': [ {'text': el.text_content(), 'href': el.attrib['href']} for el in el[1].xpath('./a') ], 'Title': extract_title(el[2]), 'Publisher': el[3].text, 'Year': el[4].text, 'Pages': el[5].text, 'Language': el[6].text, 'Size': el[7].text, 'Extension': el[8].text, 'Mirrors': [el[0].attrib['href'] for el in el[9:-1]], 'Edit': { 'href': el[-1][0].attrib['href'], 'title': el[-1][0].attrib['title'], } } for el in table[1:]]

def gen_query( query_params: Union[str, dict], only_md5: bool = False, origin: str = 'http://libgen.rs',) -> Generator: '''通过一些查询参数,查询相关书本的列表信息
:param query_params: 查询参数,就是网页接口可接受的查询参数。如果未指定参数 page,那么 page 会被设为 1。 循环递增 page,并且把相应页面的书本信息迭代返回,直到新的页面里面没有书本信息 :param only_md5: 是否只需要书本的 md5 :param origin: 源 [Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin)
:return: 如果 only_md5 为真,返回 Generator[str, None, None],是书本 md5 的列表; 否则,返回 Generator[dict, None, None],是书本的列表信息 ''' if isinstance(query_params, str): query_params = dict(parse_qsl(query_params)) if 'page' in query_params: page = query_params['page'] = int(query_params['page']) else: page = query_params['page'] = 1 result = query(query_params, only_md5=only_md5, origin=origin) while result: yield from result page += 1 query_params['page'] = page result = query(query_params, only_md5=only_md5, origin=origin)

def query_info( md5: str, origin: str = 'http://libgen.rs',) -> dict: '''根据 md5 查询一本书的详细信息
:param md5: 书本的 md5 值 :param origin: 源 [Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin)
:return: 这本书的详细信息的字典 ''' url = f'{origin}/book/index.php?md5={md5}' context_url = URL(url)
def extract_field_text(el): return el.text_content().rstrip(': ')
def extract_el_a(el, callback=None): info = { 'href': absolute_url(el.attrib['href'], context_url), 'text': ' '.join(el.itertext()) } if callback: info.update(callback(el)) return info
def extract_nested_el_table(el): return dict(zip( filter(None, map(extract_field_text, el[0])), map(extract_field_value, el[1]), ))
def extract_field_value(el): if len(el): el = el[0] if el.tag == 'a': return extract_el_a(el) elif el.tag == 'table': return extract_nested_el_table(el) return el.text_content().strip()
def extract_el_a_input_filename(el): el = el.getparent().find('input') if el is None: return [] return [('filename', el.attrib.get('value', ''))]
info: dict = {'URL': url}
r = fetch(url)
tree = fromstring(r.content) table = tree.find('.//table')
td = table[1][0]
info['DOWNLOAD_PAGE'] = absolute_url( td.find('./a').attrib['href'], context_url) info['COVER'] = absolute_url( td.find('./a/img').attrib['src'], context_url) info['HASHES'] = dict(zip( td.xpath('./table/tr/th/text()'), td.xpath('./table/tr/td/text()'), ))
detail = info['DETAIL'] = {}
detail[extract_field_text(table[1][1])] = { 'href': absolute_url( table[1][2].find('.//a').attrib['href'], context_url), 'text': table[1][2].find('.//a').text, 'extend': [ {extract_field_text(table[1][3]): table[1][3][0].tail }] }
detail.update(zip( map( extract_field_text, table.xpath( 'tr[position()>2 and position()<18]/td[position() mod 2 = 1]') ), map( extract_field_value, table.xpath( 'tr[position()>2 and position()<18]/td[position() mod 2 = 0]') ), ))
detail[extract_field_text(table[17][0])] = [ extract_el_a(el, extract_el_a_input_filename) for el in table[17][1][0].xpath('.//td/a') ]
return info

def query_downlink( md5: str, origin: str = 'http://library.lol', _cre=re_compile('(?<=href=")[^"]+'),) -> str: '''根据 md5 查询一本书的下载链接
:param md5: 书本的 md5 值 :param origin: 源 [Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin)
:return: 下载链接,如果未能取得下载链接,抛出异常 DownloadLinkNotFound ''' url = f'{origin}/main/{md5}' r = fetch(url) match = _cre.search(r.text) if match is None: raise DownloadLinkNotFound(url) return match[0]

if __name__ == '__main__': import os import platform
from argparse import ArgumentParser, RawTextHelpFormatter from concurrent.futures import ThreadPoolExecutor from os import getenv, get_terminal_size, terminal_size from sys import stderr, stdout from threading import RLock from time import perf_counter
# Reference: # - [How to get Linux console window width in Python](https://stackoverflow.com/questions/566746/how-to-get-linux-console-window-width-in-python) # - [How do I find the width & height of a terminal window](https://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window)
def environ_GWINSZ() -> terminal_size: # COLUMNS, LINES are the working values return terminal_size(int(getenv(var, 0)) for var in ('COLUMNS', 'LINES'))
def os_GWINSZ(fd: int = stdout.fileno()) -> terminal_size: # Reference: # - [os.get_terminal_size](https://docs.python.org/3/library/os.html#os.get_terminal_size) # - [shutil.get_terminal_size](https://docs.python.org/3/library/shutil.html#shutil.get_terminal_size) try: return get_terminal_size(fd) except (AttributeError, ValueError, OSError): # fd is nonexists, closed, detached, or not a terminal, or # os.get_terminal_size() is unsupported # Tips: If fd is nonexists, closed, detached, or not a terminal, # then it may raise the following exception # OSError: [Errno 25] Inappropriate ioctl for device return terminal_size((0, 0))
def ioctl_GWINSZ(fd: int = stdout.fileno()) -> terminal_size: try: from fcntl import ioctl from struct import unpack from termios import TIOCGWINSZ
rows, columns, hp, wp = unpack('hhhh', ioctl(fd, TIOCGWINSZ, b'\0'*8)) return terminal_size((columns, rows)) except (ImportError, AttributeError, ValueError, OSError): # fd is nonexists, closed, detached, or not a terminal, or # related modules are unsupported # Tips: If fd is nonexists, closed, detached, or not a terminal, # then it may raise the following exception # OSError: [Errno 25] Inappropriate ioctl for device return terminal_size((0, 0))
def ioctl_GWINSZ_auto() -> terminal_size: for size in map(ioctl_GWINSZ, range(3)): if size != (0, 0): return size
try: fd = os.open(os.ctermid(), os.O_RDONLY) try: return ioctl_GWINSZ(fd) finally: os.close(fd) except: return terminal_size((0, 0))
def stty_GWINSZ() -> terminal_size: import subprocess try: rows, columns = subprocess.check_output(['stty', 'size']).split() return terminal_size((int(columns), int(rows))) except: # If it is working on a script that expects redirected input on stdin, # and stty would complain that "stdin isn't a terminal" in that case. try: with open('/dev/tty') as tty: rows, columns = subprocess.check_output( ['stty', 'size'], stdin=tty).split() return terminal_size((int(columns), int(rows))) except: # maybe stty is unsupported return terminal_size((0, 0))
def tput_GWINSZ() -> terminal_size: try: import subprocess rows = int(subprocess.check_output(['tput', 'lines'])) columns = int(subprocess.check_output(['tput', 'cols'])) return terminal_size((columns, rows)) except: # maybe tput is unsupported return terminal_size((0, 0))
def curses_GWINSZ() -> terminal_size: try: import curses rows, columns = curses.initscr().getmaxyx() return terminal_size((columns, rows)) except: return terminal_size((0, 0))
def windows_GWINSZ() -> terminal_size: if platform.system() != 'Windows': return terminal_size((0, 0))
try: from ctypes import windll, create_string_buffer # type: ignore
# stdin handle is -10 # stdout handle is -11 # stderr handle is -12
h = windll.kernel32.GetStdHandle(-12) csbi = create_string_buffer(22) res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) except: return terminal_size((0, 0))
if res: import struct (bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw) sizex = right - left + 1 sizey = bottom - top + 1 return terminal_size((sizey, sizex)) else: return terminal_size((0, 0))
def get_columns_size(fallback=80, fd=stdout.fileno()): columns = environ_GWINSZ().columns if columns > 0: return columns
columns = os_GWINSZ(fd).columns if columns > 0: return columns
columns = stty_GWINSZ().columns if columns > 0: return columns
return fallback
ap = ArgumentParser( description='libgen 下载链接提取', formatter_class=RawTextHelpFormatter, epilog='【温馨提示】\n' '下载链接会被输出到标准输出 stdout,异常、失败和进度条会被输出到标准错误 ' 'stderr。因此如果在命令的末尾增加 >downlinks.txt,就会把下载链接输出到' '文本文件 downlinks.txt 中,而命令行中依旧输出异常、失败和进度条。', ) ap.add_argument('query_string', nargs=1, help='查询字符串') ap.add_argument( '-c', '--complex', action='store_true', help='如果【不指定】此参数,只会把查询字符串 query_string 视为请求时' '携带的查询字符串中的一个关键字参数 req;\n' '如果【指定】此参数,会把查询字符串 query_string 视为' '请求时携带的查询字符串' ) ap.add_argument( '-o', '--only-md5', dest='only_md5', action='store_true', help='只需要采集md5信息' ) ap.add_argument( '-m', '--max-workers', dest='max_workers', default=None, type=int, help='提取下载链接时工作的最大线程数,默认是 None,此时 ' 'max_workers = min(32, (os.cpu_count() or 1) + 4)' ) ap.add_argument( '-r', '--max-request-times', dest='max_request_times', default=10, type=int, help='网络请求时的最大尝试次数, 默认是10' ) args = ap.parse_args()
# Reference: # - [tqdm](https://pypi.org/project/tqdm/) # - [rich](https://pypi.org/project/rich/) # - [blessings](https://pypi.org/project/blessings/) # - [colorama](https://pypi.org/project/colorama/)
class ProgressInfo:
def __init__(self): self._total: int = 0 self._success: int = 0 self._failed: int = 0 self._str: str = '' self._size: int = 0 self._current_ts = self._start_ts = perf_counter() self._lock = RLock()
@property def col_total(self) -> str: return f'🤔 Total: {self._total}'
@property def col_success(self) -> str: return f'😂 Success: {self._success}'
@property def col_failed(self) -> str: return f'😭 Failed: {self._failed}'
@property def col_speed(self) -> str: elapsed = self._current_ts - self._start_ts if elapsed == 0: speed = 'nan' else: speed = format(self._total / elapsed, '.6f') return f'🚀 Speed: {speed} i/s'
@property def col_elapsed(self) -> str: return f'🕙 Elapsed: {self._current_ts - self._start_ts:.6f} s'
@property def col_success_rate(self) -> str: if self._total: rate = self._success * 100 / self._total else: rate = 100 return f'💯 Succeess Rate: {rate:.2f}%'
def tostring(self) -> Tuple[int, str]: columns: int = get_columns_size(fd=stderr.fileno()) cols: list = [] col_expand_size: int = 0 while True: # ' ' takes up 1 columns columns -= 1 if columns <= 0: break col = self.col_failed # '😭' takes up 2 columns, 1 extra columns -= len(col) + 1 if columns < 0: break cols.append(col) col_expand_size += 1
# ' | ' takes up 3 columns columns -= 3 if columns <= 0: break col = self.col_success # '😂' takes up 2 columns, 1 extra columns -= len(col) + 1 if columns < 0: break cols.insert(0, col) col_expand_size += 1
# ' | ' takes up 3 columns columns -= 3 if columns <= 0: break col = self.col_speed # '🚀' takes up 2 columns, 1 extra columns -= len(col) + 1 if columns < 0: break cols.append(col) col_expand_size += 1
# ' | ' takes up 3 columns columns -= 3 if columns <= 0: break col = self.col_success_rate # '💯' takes up 2 columns, 1 extra columns -= len(col) + 1 if columns < 0: break cols.insert(2, col) col_expand_size += 1
# ' | ' takes up 3 columns columns -= 3 if columns <= 0: break col = self.col_total # '🤔' takes up 2 columns, 1 extra columns -= len(col) + 1 if columns < 0: break cols.insert(0, col) col_expand_size += 1
# ' | ' takes up 3 columns columns -= 3 if columns <= 0: break col = self.col_elapsed # '🕙' takes up 2 columns, 1 extra columns -= len(col) + 1 if columns < 0: break cols.append(col) col_expand_size += 1
break
s = f' %s\r' % ' | '.join(cols) # '\r' takes up 0 columns, -1 extra return len(s) - 1 + col_expand_size, s
def update(self) -> None: with self._lock: self.clear() self._current_ts = perf_counter() self._size, self._str = self.tostring() self.output()
def inc_success(self) -> None: with self._lock: self._success += 1 self._total += 1 self.update()
def inc_failed(self) -> None: with self._lock: self._failed += 1 self._total += 1 self.update()
def clear(self) -> None: if self._size: with self._lock: stderr.write(' '*self._size) #stderr.write('\b'*self._size) stderr.write('\r') stderr.flush()
def output(self) -> None: with self._lock: stderr.write(self._str) stderr.flush()
def pure_print(self, *args, **kwds) -> None: kwds['flush'] = True with self._lock: self.clear() print(*args, **kwds) self._size = 0
def print(self, *args, **kwds) -> None: with self._lock: self.pure_print(*args, **kwds) self.output()
p = ProgressInfo()
def output_downlink(md5: str): try: r = query_downlink(md5) except BaseException as exc: p.pure_print('[FAILED]::', exc, file=stderr) p.inc_failed() else: p.pure_print(r) p.inc_success()
if args.complex: req = args.query_string[0] else: req = {'req': args.query_string[0]}
if args.only_md5: for md5 in gen_query(req, only_md5=True): p.pure_print(md5) p.inc_success() else: MAX_TIMES = args.max_request_times MAX_WORKERS = args.max_workers
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: executor.map(output_downlink, gen_query(req, only_md5=True))


你可以执行python libgen.py -h 查看参数:


PS D:\projects\python_projects\libgen> python .\libgen检索.py -husage: libgen检索.py [-h] [-c] [-o] [-m MAX_WORKERS] [-r MAX_REQUEST_TIMES] query_string
libgen 下载链接提取
positional arguments: query_string 查询字符串
optional arguments: -h, --help show this help message and exit -c, --complex 如果【不指定】此参数,只会把查询字符串 query_string 视为请求时携带的查询字符串中的一个关键字参数 req; 如果【指定】此参数,会把查询字符串 query_string 视为请求时携带的查询字符串 -o, --only-md5 只需要采集md5信息 -m MAX_WORKERS, --max-workers MAX_WORKERS 提取下载链接时工作的最大线程数,默认是 None,此时 max_workers = min(32, (os.cpu_count() or 1) + 4) -r MAX_REQUEST_TIMES, --max-request-times MAX_REQUEST_TIMES 网络请求时的最大尝试次数, 默认是10
【温馨提示】下载链接会被输出到标准输出 stdout,异常、失败和进度条会被输出到标准错误 stderr。因此如果在命令的末尾增加 >downlinks.txt,就会把下载链接输出到文本文件 downlinks.txt 中,而命令行中依旧输出异常、失败和进度条。

几个重点:

  •  如果【不指定】此参数,只会把查询字符串 query_string 视为请求时携带的查询字符串中的一个关键字参数 req;

  • 如果【指定】此参数,会把查询字符串 query_string 视为请求时携带的查询字符串

  • 在命令的末尾增加 >downlinks.txt,就会把下载链接输出到文本文件 downlinks.txt 中


批量下载

执行libgen.py脚本如下:


录下新增 downlinks.txt 文件,文件中有下载链接列表:
http://31.42.184.140/main/2221000/0bcf18c54db3b094219d976c61a56a26/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2010%29%20%5B%E8%8B%B1%5D%E5%85%8B%E9%87%8C%E6%96%AF%C2%B7%E9%BA%A6%E5%85%8B%E9%A9%AC%E7%BA%B3%E6%96%AF_%20%E8%83%A1%E6%96%B0%E5%92%8C%28%E8%AF%91%29%20-%20%E5%8F%B3%E6%89%8B%EF%BC%8C%E5%B7%A6%E6%89%8B%EF%BC%9A%E5%A4%A7%E8%84%91%E3%80%81%E8%BA%AB%E4%BD%93%E3%80%81%E5%8E%9F%E5%AD%90%E5%92%8C%E6%96%87%E5%8C%96%E4%B8%AD%E4%B8%8D%E5%AF%B9%E7%A7%B0%E6%80%A7%E7%9A%84%E8%B5%B7%E6%BA%90-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282007%29.pdfhttp://31.42.184.140/main/2221000/9934cc37d62c77e193065d909399b90d/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2006%29%20%5B%E8%8B%B1%5D%E5%B8%95%E7%89%B9%E9%87%8C%E5%85%8B%C2%B7%E6%91%A9%E5%B0%94_%20%E5%AE%8B%E5%AE%87%E8%8E%B9%28%E8%AF%91%29_%20%E5%88%98%E8%8C%9C%28%E8%AF%91%29%20-%20%E7%81%AB%E6%98%9F%E7%9A%84%E6%95%85%E4%BA%8B-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282004%29.pdfhttp://31.42.184.140/main/2221000/7d2b74efc5d08f99c864a86f2d40fdd9/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2009%29%20%5B%E8%8B%B1%5D%E9%A9%AC%E7%89%B9%C2%B7%E9%87%8C%E5%BE%B7%E5%88%A9_%20%E9%99%88%E8%99%8E%E5%B9%B3%28%E8%AF%91%29_%20%E4%B8%A5%E6%88%90%E8%8A%AC%28%E8%AF%91%29%20-%20%E5%85%88%E5%A4%A9%EF%BC%8C%E5%90%8E%E5%A4%A9%EF%BC%9A%E5%9F%BA%E5%9B%A0%E3%80%81%E7%BB%8F%E9%AA%8C%E5%92%8C%E4%BB%80%E4%B9%88%E4%BD%BF%E6%88%91%E4%BB%AC%E6%88%90%E4%B8%BA%E4%BA%BA-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282005%29.pdfhttp://31.42.184.140/main/2221000/d35a8ec3438f9673703408191432a65c/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2008%29%20%5B%E7%BE%8E%5D%E5%A8%81%E5%BB%89%E5%A7%86%C2%B7%E5%BA%9E%E5%BE%B7%E6%96%AF%E9%80%9A_%20%E6%9D%8E%E5%A4%A7%E5%BC%BA%28%E8%AF%91%29%20-%20%E6%8E%A8%E7%90%86%E7%9A%84%E8%BF%B7%E5%AE%AB%EF%BC%9A%E6%82%96%E8%AE%BA%E3%80%81%E8%B0%9C%E9%A2%98%EF%BC%8C%E5%8F%8A%E7%9F%A5%E8%AF%86%E7%9A%84%E8%84%86%E5%BC%B1%E6%80%A7-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282005%29.pdfhttp://31.42.184.140/main/2221000/23efd41b4964a7e8c9d9c5d809d5a2f0/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2002%29%20%5B%E8%8B%B1%5D%E9%A9%AC%E7%89%B9%C2%B7%E9%87%8C%E5%BE%B7%E5%88%A9_%20%E5%88%98%E8%8F%81%28%E8%AF%91%29%20-%20%E5%9F%BA%E5%9B%A0%E7%BB%84%EF%BC%9A%E4%BA%BA%E7%A7%8D%E8%87%AA%E4%BC%A023%E7%AB%A0-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282003%29.pdfhttp://31.42.184.140/main/2221000/4456038b9dd98e56fa87b45ec80ab965/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2013%29%20John%20A.%20Wheeler_%20%5B%E7%BE%8E%5D%E7%BA%A6%E7%BF%B0%C2%B7%E9%98%BF%E5%A5%87%E5%8D%9A%E5%B0%94%E5%BE%B7%C2%B7%E6%83%A0%E5%8B%92_%20%E7%94%B0%E6%9D%BE%28%E8%AF%91%29_%20%E5%8D%97%E5%AE%AB%E6%A2%85%E8%8A%B3%28%E8%AF%91%29%20-%20%E5%AE%87%E5%AE%99%E9%80%8D%E9%81%A5-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282006%29.pdfhttp://31.42.184.140/main/2221000/3c23f246cf9d788e30770abb77c62e8a/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2004%29%20%5B%E7%BE%8E%5D%E7%BA%A6%E7%BF%B0%C2%B7C%C2%B7%E6%B3%B0%E5%8B%92_%20%E6%9A%B4%E6%B0%B8%E5%AE%81%28%E8%AF%91%29%20-%20%E8%87%AA%E7%84%B6%E8%A7%84%E5%BE%8B%E4%B8%AD%E8%95%B4%E8%93%84%E7%9A%84%E7%BB%9F%E4%B8%80%E6%80%A7-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282004%29.pdfhttp://31.42.184.140/main/2221000/43ec99b1d66955e65fc31a28311065d4/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2012%29%20%5B%E7%BE%8E%5D%E7%88%B1%E5%BE%B7%E5%8D%8E%C2%B7O%C2%B7%E5%A8%81%E5%B0%94%E9%80%8A_%20%E6%AF%9B%E7%9B%9B%E8%B4%A4%20%E7%AD%89%28%E8%AF%91%29%20-%20%E7%A4%BE%E4%BC%9A%E7%94%9F%E7%89%A9%E5%AD%A6%EF%BC%9A%E6%96%B0%E7%9A%84%E7%BB%BC%E5%90%88-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282008%29.pdfhttp://31.42.184.140/main/2221000/e1e9efa718689d612a2649207c879cb9/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2005%29%20%5B%E5%BE%B7%5DV%C2%B7%E9%98%BF%E5%B0%94%E8%8C%A8%E7%89%B9_%20I%C2%B7%E6%AF%94%E5%B0%94%E6%A2%85%E6%9E%97_%20%E9%A9%AC%E6%80%80%E7%90%AA%28%E8%AF%91%29_%20%E9%99%88%E7%90%A6%28%E8%AF%91%29%20-%20%E5%8A%A8%E7%89%A9%E6%9C%89%E6%84%8F%E8%AF%86%E5%90%97-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282004%29.pdfhttp://31.42.184.140/main/2221000/423bd1e530ecca521092b0cb3bec43f5/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2017%29%20%5B%E7%BE%8E%5D%E4%B8%B9%E5%B0%BC%E5%B0%94%C2%B7%E4%B8%B9%E5%B0%BC%E7%89%B9_%20%E8%8B%8F%E5%BE%B7%E8%B6%85%20%E7%AD%89%28%E8%AF%91%29%20-%20%E6%84%8F%E8%AF%86%E7%9A%84%E8%A7%A3%E9%87%8A-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282008%29.pdfhttp://31.42.184.140/main/2221000/b18aba918d8ed2a14d6965f2a42cd77a/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2015%29%20%5B%E7%BE%8E%5D%E7%BD%97%E4%BC%AF%E7%89%B9%C2%B7J%C2%B7%E6%96%AF%E6%BB%95%E5%8D%9A%E6%A0%BC_%20%E7%8E%8B%E5%88%A9%E7%BE%A4%28%E8%AF%91%29%20-%20%E6%99%BA%E6%85%A7%E6%99%BA%E5%8A%9B%E5%88%9B%E9%80%A0%E5%8A%9B-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282007%29.pdfhttp://31.42.184.140/main/2221000/c3a386ff65ecaf126461d029acac924e/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2003%29%20%5B%E8%8B%B1%5D%E9%A9%AC%E5%85%8B%C2%B7%E9%87%8C%E5%BE%B7%E5%88%A9%20_%20%E4%BD%95%E6%9C%9D%E9%98%B3%28%E8%AF%91%29%20_%20%E6%9E%97%E7%88%B1%E5%85%B5%28%E8%AF%91%29%20-%20%E5%AD%9F%E5%BE%B7%E5%B0%94%E5%A6%96%EF%BC%9A%E5%9F%BA%E5%9B%A0%E7%9A%84%E5%85%AC%E6%AD%A3%E4%B8%8E%E7%94%9F%E5%91%BD%E7%9A%84%E5%A4%8D%E6%9D%82-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282004%29.pdfhttp://31.42.184.140/main/2221000/4e464c67b4355b2e5afbb85cf68e7559/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2001%29%20%5B%E7%BE%8E%5D%E6%B3%BD%E5%B8%83%E7%BD%97%E5%A4%AB%E6%96%AF%E5%9F%BA_%20%E6%9D%8E%E5%A4%A7%E5%BC%BA%28%E8%AF%91%29%20-%20%E5%9C%86%E7%9A%84%E5%8E%86%E5%8F%B2%EF%BC%9A%E6%95%B0%E5%AD%A6%E6%8E%A8%E7%90%86%E4%B8%8E%E7%89%A9%E7%90%86%E5%AE%87%E5%AE%99-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282003%29.pdfhttp://31.42.184.140/main/2221000/d32e8ad3c952bb7b09aecb3286a2edbb/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2014%29%20%5B%E7%BE%8E%5D%E5%A8%81%E5%BB%89%E5%A7%86%C2%B7%E5%BA%9E%E5%BE%B7%E6%96%AF%E9%80%9A_%20%E5%90%B4%E9%B9%A4%E9%BE%84%28%E8%AF%91%29%20-%20%E5%9B%9A%E5%BE%92%E7%9A%84%E5%9B%B0%E5%A2%83%EF%BC%9A%E5%86%AF%C2%B7%E8%AF%BA%E4%BC%8A%E6%9B%BC%E3%80%81%E5%8D%9A%E5%BC%88%E8%AE%BA%EF%BC%8C%E5%92%8C%E5%8E%9F%E5%AD%90%E5%BC%B9%E4%B9%8B%E8%B0%9C-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282005%29.pdfhttp://31.42.184.140/main/2221000/ad5cb09269447542ca035d85819a6d44/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2011%29%20%5B%E7%BE%8E%5D%E6%96%AF%E6%BB%95%E5%8D%9A%E6%A0%BC_%20%E6%96%BD%E5%BB%BA%E5%86%9C%20%E7%AD%89%28%E8%AF%91%29%20-%20%E5%88%9B%E9%80%A0%E5%8A%9B%E6%89%8B%E5%86%8C-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282005%29.pdfhttp://31.42.184.140/main/2221000/655c31c9f912f2b9b4635ece011dd927/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2016%29%20%5B%E5%8A%A0%5DP%C2%B7%E6%8B%89%E5%8F%A4%E5%BE%B7_%20%5B%E7%BE%8E%5DJ%C2%B7%E5%B8%83%E5%8B%92%E6%A3%AE_%20%E9%99%88%E6%9D%A5%E9%A3%9E%28%E8%AF%91%29_%20%E5%90%B4%E8%8E%89%28%E8%AF%91%29%20-%20%E6%8B%BF%E7%A0%B4%E4%BB%91%E7%9A%84%E7%BA%BD%E6%89%A3%EF%BC%9A%E6%94%B9%E5%8F%98%E5%8E%86%E5%8F%B2%E7%9A%8416%E4%B8%AA%E5%8C%96%E5%AD%A6%E6%95%85%E4%BA%8B-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282007%29.pdfhttp://31.42.184.140/main/2221000/c375d15aa39d18b0a7c85e2729fde6bd/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%2007%29%20%5B%E8%8B%B1%5D%E5%B8%95%E7%89%B9%E9%87%8C%E5%85%8B%C2%B7%E6%91%A9%E5%B0%94_%20%E9%A9%AC%E6%98%9F%E5%9E%A3%28%E8%AF%91%29_%20%E5%82%85%E5%BE%B7%E8%AD%A7%28%E8%AF%91%29%20-%20%E6%9C%88%E7%90%83%E7%9A%84%E6%95%85%E4%BA%8B-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282005%29.djvuhttp://31.42.184.140/main/2689000/16640ca810e235f40f7b7ef37cc5814c/%28%E7%9B%97%E7%81%AB%E8%80%85%E8%AF%91%E4%B8%9B%29%20%5B%E7%BE%8E%5D%20%E6%91%A9%E5%B0%94%20-%20%E7%81%AB%E6%98%9F%E7%9A%84%E6%95%85%E4%BA%8B-%E5%8C%97%E4%BA%AC%E7%90%86%E5%B7%A5%E5%A4%A7%E5%AD%A6%E5%87%BA%E7%89%88%E7%A4%BE%20%282004%29.pdf


全选复制这些链接,打开你的下载器。

我用的是IDM,点击任务-->从剪贴板添加批量下载任务。如:

用不了半小时,这些电子书就安安静静的躺在我的硬盘里。

至于... 什么时候读?这个一个问题...

往期推荐

Markdown完全教程

python专家访谈录01 布雷特·坎农

如何设计并实现JavaScript语言【中英字幕】

你心心念念的必应壁纸,可以一键打包下载了...

盗火者译丛全17册(囚徒的困境、火星的故事、圆的历史、意识的解释...)

继续滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存