其他
Python脚本批量下载创世纪图书馆电子书
文章内容
1. 创世纪图书馆简介
2. Libgen.py及使用方法
3. 批量下载
创世纪图书馆(英语: Library Genesis,缩写为LibGen)是科学论文及书籍的搜索引擎,可以免费提供被挡在付费墙(paywall)后的内容。截止2018年6月,创世纪图书数据库宣称其包含超过270万册图书,以及超过5800万篇期刊文献。
创世纪图书馆
http://libgen.lc/search.php?req=盗火者译丛&open=0&res=25&view=simple&phrase=1&column=def
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 requests
import urllib3 # type: ignore
from functools import partial, update_wrapper
from itertools import chain
from re import compile as re_compile
from typing import (
cast, Callable, Generator, List, Optional,
Tuple, Type, Union,
)
from urllib.parse import parse_qsl
from lxml.html import fromstring # type: ignore
from 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 -h
usage: 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 中
批量下载
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.pdf
http://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.pdf
http://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.pdf
http://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.pdf
http://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.pdf
http://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.pdf
http://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.pdf
http://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.pdf
http://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.pdf
http://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.pdf
http://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.pdf
http://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.pdf
http://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.pdf
http://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.pdf
http://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.pdf
http://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.pdf
http://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.djvu
http://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,点击任务-->从剪贴板添加批量下载任务。如:
至于... 什么时候读?这个一个问题...
往期推荐